@lynxship/cli 0.1.13 → 0.1.15
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/README.md +33 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/dev-qr.d.ts.map +1 -1
- package/dist/dev-qr.js +5 -3
- package/dist/harmony-build.d.ts +1 -0
- package/dist/harmony-build.d.ts.map +1 -1
- package/dist/harmony-build.js +19 -6
- package/dist/index.js +26 -6
- package/dist/ios-build.d.ts +21 -0
- package/dist/ios-build.d.ts.map +1 -1
- package/dist/ios-build.js +184 -1
- package/dist/ios-host.d.ts +2 -0
- package/dist/ios-host.d.ts.map +1 -1
- package/dist/ios-host.js +2 -0
- package/dist/ui/components.d.ts +1 -1
- package/dist/ui/components.d.ts.map +1 -1
- package/dist/ui/components.js +20 -4
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +5 -2
- package/dist/ui/qr.d.ts +61 -0
- package/dist/ui/qr.d.ts.map +1 -0
- package/dist/ui/qr.js +180 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -212,6 +212,11 @@ Next steps
|
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
`lynxship dev` is for Lynx Explorer and live QR/HMR development. The Android
|
|
215
|
+
Rspeedy URLs are rendered by LynxShip's own compact QR adapter so the QR
|
|
216
|
+
remains visible when child-process output is captured. It uses the WISA
|
|
217
|
+
renderer settings (H-level correction, dots, dotted corners and 50-degree
|
|
218
|
+
linear gradient) with LynxShip pink-to-cyan colors and no center logo;
|
|
219
|
+
non-interactive output keeps the URL and omits terminal decoration. The Android
|
|
215
220
|
host command creates the native Gradle project needed for a real APK/AAB. An
|
|
216
221
|
interactive real build creates a missing host after asking for the application
|
|
217
222
|
ID; CI must pass `--application-id`. Existing native directories are never
|
|
@@ -277,6 +282,10 @@ Use the official Lynx Harmony integration under `harmony/`. The project must
|
|
|
277
282
|
provide `hvigorw`, `hvigorfile.ts`, `build-profile.json5` and
|
|
278
283
|
`oh-package.json5`. LynxShip runs `ohpm install`, the pinned wrapper in release
|
|
279
284
|
mode, then verifies the signed HAP with the official `hap-sign-tool.jar`.
|
|
285
|
+
Before Hvigor runs, it copies root `.lynx.bundle` files plus `dist/static` and
|
|
286
|
+
`dist/async` into the configured HarmonyOS `rawfile` directory. This keeps
|
|
287
|
+
imported images and lazy bundles inside the HAP instead of leaving them only in
|
|
288
|
+
the JavaScript build output.
|
|
280
289
|
|
|
281
290
|
Optional profile overrides are explicit and remain project-owned:
|
|
282
291
|
|
|
@@ -406,6 +415,19 @@ with:
|
|
|
406
415
|
lynxship ios host init --bundle-identifier com.example.myapp
|
|
407
416
|
```
|
|
408
417
|
|
|
418
|
+
If the project already has its app icon, provide the required high-resolution
|
|
419
|
+
PNG during host creation:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
lynxship ios host init --bundle-identifier com.example.myapp --icon ./assets/icon.png
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The icon must be a square 1024x1024 PNG. For an existing host, set
|
|
426
|
+
`build.production.ios.appIcon` in `lynxship.json`, or add `icon.png` in the
|
|
427
|
+
project root; LynxShip discovers it before the next native build. Simulator
|
|
428
|
+
builds may fall back to a square `lynx-logo*.png` emitted by Rspeedy for local
|
|
429
|
+
preview only; provide the project icon for an IPA or store release.
|
|
430
|
+
|
|
409
431
|
The command refuses to overwrite an existing `ios/` directory and creates a
|
|
410
432
|
Swift host based on Lynx's official integration shape: `LynxEnv`, `LynxView`,
|
|
411
433
|
`LynxTemplateProvider`, `Podfile`, `ExportOptions.plist` and a bundle sync
|
|
@@ -421,9 +443,18 @@ lynxship build --project-dir ./my-lynx-app --platform ios --simulator --profile
|
|
|
421
443
|
```
|
|
422
444
|
|
|
423
445
|
This uses Xcode's `iphonesimulator` SDK, boots an available Simulator, creates
|
|
424
|
-
and installs a local `.app`,
|
|
446
|
+
and installs a local `.app`, opens the Simulator and launches the app in an
|
|
447
|
+
interactive terminal, and does not require a physical iPhone, Apple
|
|
425
448
|
Distribution certificate, provisioning profile or R2 upload. Use the
|
|
426
|
-
production profile only for a signed device IPA.
|
|
449
|
+
production profile only for a signed device IPA. Pass `--no-autostart` to
|
|
450
|
+
install without launching, or use `run --platform ios --simulator --launch
|
|
451
|
+
--artifact <app>` to launch an already-built app.
|
|
452
|
+
|
|
453
|
+
Rspeedy can emit external files beside the main bundle. The iOS adapter copies
|
|
454
|
+
all root `.lynx.bundle` files plus `dist/static` and `dist/async` into the
|
|
455
|
+
compiled `.app` before installation/export, so imported images and lazy assets
|
|
456
|
+
are included in Simulator and device builds. The same synchronization is
|
|
457
|
+
applied to older LynxShip-generated iOS hosts.
|
|
427
458
|
|
|
428
459
|
Run the complete first-use diagnosis before building:
|
|
429
460
|
|
package/dist/config.d.ts
CHANGED
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,GAAG,CAAC,EAAE;QACJ,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,GAAG,CAAC,EAAE;QACJ,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAChC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,cAAc,CAAC,EAAE;QAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE;YAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1C,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,cAsB5B,CAAC;AAaF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,cAAc,CAyChB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC,cAAc,CAAC,CAazB;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,cAAc,EACtB,IAAI,SAAe,GAClB,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAejC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAOrD"}
|
package/dist/dev-qr.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-qr.d.ts","sourceRoot":"","sources":["../src/dev-qr.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev-qr.d.ts","sourceRoot":"","sources":["../src/dev-qr.ts"],"names":[],"mappings":"AAOA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOpE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D"}
|
package/dist/dev-qr.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
// Rspeedy's QR plugin accepts custom schemas, so a dev URL is not always HTTP.
|
|
2
|
+
const URL_PATTERN = /[a-z][a-z\d+.-]*:\/\/[^\s<>"'`]+/gi;
|
|
2
3
|
function cleanUrl(value) {
|
|
3
4
|
return value.replace(/[),.;!?]+$/u, "");
|
|
4
5
|
}
|
|
5
6
|
export function extractDevServerUrl(line) {
|
|
6
7
|
const urls = (line.match(URL_PATTERN) ?? []).map(cleanUrl);
|
|
7
8
|
return (urls.find((url) => url.includes("fullscreen=true")) ??
|
|
8
|
-
urls.find((url) => url.includes(".lynx.bundle"))
|
|
9
|
+
urls.find((url) => url.includes(".lynx.bundle")) ??
|
|
10
|
+
urls[0]);
|
|
9
11
|
}
|
|
10
12
|
export function shouldPrintDevServerQr(line) {
|
|
11
|
-
return /(?:default:|fullscreen
|
|
13
|
+
return /(?:default:|fullscreen|\bready\b|\blynx\b)/iu.test(line);
|
|
12
14
|
}
|
package/dist/harmony-build.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare function harmonyToolchain(root: string): {
|
|
|
17
17
|
hdc: boolean;
|
|
18
18
|
message: string;
|
|
19
19
|
};
|
|
20
|
+
export declare function syncHarmonyAssets(root: string, profile: BuildProfile): Promise<string[]>;
|
|
20
21
|
export declare function runRealHarmonyBuild(job: BuildJob, options: HarmonyBuildOptions): Promise<BuildJob>;
|
|
21
22
|
export {};
|
|
22
23
|
//# sourceMappingURL=harmony-build.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harmony-build.d.ts","sourceRoot":"","sources":["../src/harmony-build.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAeD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC9C,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAcA;
|
|
1
|
+
{"version":3,"file":"harmony-build.d.ts","sourceRoot":"","sources":["../src/harmony-build.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAeD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC9C,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAcA;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC,CA4BnB;AA2GD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAoGnB"}
|
package/dist/harmony-build.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyFile, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
1
|
+
import { copyFile, cp, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
@@ -37,7 +37,7 @@ export function harmonyToolchain(root) {
|
|
|
37
37
|
: "Harmony host requires the project hvigorw wrapper and ohpm from DevEco Studio",
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
async function
|
|
40
|
+
export async function syncHarmonyAssets(root, profile) {
|
|
41
41
|
const dist = join(root, "dist");
|
|
42
42
|
const entries = await readdir(dist, { withFileTypes: true }).catch(() => []);
|
|
43
43
|
const bundles = entries
|
|
@@ -46,9 +46,20 @@ async function syncBundle(root, profile) {
|
|
|
46
46
|
assert(bundles.length > 0, "LYNX_BUNDLE_MISSING", `No .lynx.bundle was found in ${dist}. Check the Rspeedy output configuration.`);
|
|
47
47
|
const target = resolve(root, profile.harmony?.bundleDir ?? "harmony/entry/src/main/resources/rawfile");
|
|
48
48
|
await mkdir(target, { recursive: true });
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const outputNames = [...bundles, "async", "static"];
|
|
50
|
+
const copied = [];
|
|
51
|
+
for (const name of outputNames) {
|
|
52
|
+
const source = join(dist, name);
|
|
53
|
+
if (!existsSync(source))
|
|
54
|
+
continue;
|
|
55
|
+
await rm(join(target, name), { recursive: true, force: true });
|
|
56
|
+
if (name.endsWith(".lynx.bundle"))
|
|
57
|
+
await copyFile(source, join(target, name));
|
|
58
|
+
else
|
|
59
|
+
await cp(source, join(target, name), { recursive: true, force: true });
|
|
60
|
+
copied.push(name);
|
|
61
|
+
}
|
|
62
|
+
return copied;
|
|
52
63
|
}
|
|
53
64
|
async function findHap(root, configured) {
|
|
54
65
|
if (configured) {
|
|
@@ -133,7 +144,9 @@ export async function runRealHarmonyBuild(job, options) {
|
|
|
133
144
|
});
|
|
134
145
|
}
|
|
135
146
|
step("Syncing bundle into the HarmonyOS HAP resources…", 20);
|
|
136
|
-
await
|
|
147
|
+
const copiedAssets = await syncHarmonyAssets(options.root, options.profile);
|
|
148
|
+
for (const name of copiedAssets)
|
|
149
|
+
options.onEvent?.(`Synced dist/${name} into HarmonyOS rawfile resources`);
|
|
137
150
|
transitionBuild(job, "queued", "HarmonyOS build queued locally");
|
|
138
151
|
transitionBuild(job, "provisioning", "HarmonyOS Hvigor toolchain selected");
|
|
139
152
|
transitionBuild(job, "installing_dependencies", "HarmonyOS dependencies selected");
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { DEFAULT_CONFIG, loadConfig, platformValue, resolveProfile, } from "./co
|
|
|
12
12
|
import { hasAndroidHost, isSupportedAndroidPlatform, runRealAndroidBuild, } from "./android-build.js";
|
|
13
13
|
import { initializeAndroidHost, suggestedAndroidApplicationId, } from "./android-host.js";
|
|
14
14
|
import { initializeIosHost, suggestedIosBundleIdentifier } from "./ios-host.js";
|
|
15
|
-
import { hasIosHost, runRealIosBuild } from "./ios-build.js";
|
|
15
|
+
import { hasIosHost, launchIosSimulatorApp, runRealIosBuild, } from "./ios-build.js";
|
|
16
16
|
import { configureAndroid, configureAppStoreConnect, configureGooglePlay, configureR2, } from "./configure.js";
|
|
17
17
|
import { fetchOtaPublicKey, publishOtaRelease, rollbackOtaRelease, submitRealArtifact, } from "./remote.js";
|
|
18
18
|
import { uploadR2Artifact } from "./r2.js";
|
|
@@ -209,7 +209,7 @@ async function saveState(state, repository, builds, submissions) {
|
|
|
209
209
|
}
|
|
210
210
|
let stateSaveQueue = Promise.resolve();
|
|
211
211
|
async function executeBuild(options, showResult = true) {
|
|
212
|
-
const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, simulator = false, simulatorDevice, } = options;
|
|
212
|
+
const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, simulator = false, simulatorDevice, simulatorAutostart = false, } = options;
|
|
213
213
|
if (wait && !local)
|
|
214
214
|
await ensureNativeHostForBuild(platform, profile, config);
|
|
215
215
|
// Host initialization can write lynxship.json (for example, the generated
|
|
@@ -281,6 +281,7 @@ async function executeBuild(options, showResult = true) {
|
|
|
281
281
|
profile: resolvedBuildProfile,
|
|
282
282
|
simulator,
|
|
283
283
|
simulatorDevice,
|
|
284
|
+
simulatorAutostart,
|
|
284
285
|
uploadArtifacts: !skipUpload,
|
|
285
286
|
skipBundleBuild,
|
|
286
287
|
quiet: json,
|
|
@@ -459,6 +460,8 @@ Build options:
|
|
|
459
460
|
--no-upload Keep the artifact local and skip R2 (CI verification)
|
|
460
461
|
--simulator Build and install an iOS Simulator .app locally
|
|
461
462
|
--device <id> Select the iOS Simulator device for a simulator build
|
|
463
|
+
--autostart Open and launch the iOS Simulator app after install
|
|
464
|
+
--no-autostart Install the Simulator app without launching it
|
|
462
465
|
--allow-unsigned Allow an unsigned Desktop artifact only with --no-upload (local tests)
|
|
463
466
|
--local Use the contract-only build path for tests
|
|
464
467
|
|
|
@@ -490,6 +493,7 @@ Device and diagnostics options:
|
|
|
490
493
|
run --artifact <path> Install a specific APK, IPA, app or signed HAP
|
|
491
494
|
run --device <id> Select an Android, iOS or HarmonyOS device
|
|
492
495
|
run --simulator Install an iOS .app with simctl
|
|
496
|
+
run --launch Launch the installed iOS Simulator app after install
|
|
493
497
|
logs --device <id> Select the device or simulator for native logs
|
|
494
498
|
|
|
495
499
|
Global options:
|
|
@@ -503,6 +507,7 @@ Global options:
|
|
|
503
507
|
--project-id <id> Project ID used by init
|
|
504
508
|
--application-id <id> Android package/application ID for host init
|
|
505
509
|
--bundle-identifier <id> iOS bundle identifier for host init
|
|
510
|
+
--icon <path> 1024x1024 PNG app icon for iOS host init
|
|
506
511
|
--library-dir <path> Native library directory for autolink codegen
|
|
507
512
|
--simulator Build/install an iOS .app or install one with simctl
|
|
508
513
|
--help Show this complete command reference
|
|
@@ -594,6 +599,7 @@ async function ensureNativeHostForBuild(platform, profile, config) {
|
|
|
594
599
|
const result = await initializeIosHost(root, {
|
|
595
600
|
bundleIdentifier,
|
|
596
601
|
appName: basename(root),
|
|
602
|
+
appIcon: flag("--icon") ?? undefined,
|
|
597
603
|
});
|
|
598
604
|
ui.success(`iOS host created: ${result.directory}`);
|
|
599
605
|
}
|
|
@@ -638,13 +644,13 @@ async function runRspeedyCommand(subcommand, environment) {
|
|
|
638
644
|
let devUrl;
|
|
639
645
|
let devQrPrinted = false;
|
|
640
646
|
let devQrTimer;
|
|
641
|
-
const printDevQr = () => {
|
|
647
|
+
const printDevQr = async () => {
|
|
642
648
|
if (!devUrl || devQrPrinted || json || ui.options.quiet)
|
|
643
649
|
return;
|
|
644
650
|
devQrPrinted = true;
|
|
645
651
|
if (devQrTimer)
|
|
646
652
|
clearTimeout(devQrTimer);
|
|
647
|
-
ui.devServerQr(devUrl);
|
|
653
|
+
await ui.devServerQr(devUrl);
|
|
648
654
|
};
|
|
649
655
|
await runRspeedy(root, subcommand, forwarded, {
|
|
650
656
|
env: environment,
|
|
@@ -658,10 +664,12 @@ async function runRspeedyCommand(subcommand, environment) {
|
|
|
658
664
|
devUrl = url;
|
|
659
665
|
if (devQrTimer)
|
|
660
666
|
clearTimeout(devQrTimer);
|
|
661
|
-
devQrTimer = setTimeout(
|
|
667
|
+
devQrTimer = setTimeout(() => {
|
|
668
|
+
void printDevQr();
|
|
669
|
+
}, 500);
|
|
662
670
|
}
|
|
663
671
|
if (devUrl && shouldPrintDevServerQr(line))
|
|
664
|
-
printDevQr();
|
|
672
|
+
void printDevQr();
|
|
665
673
|
},
|
|
666
674
|
});
|
|
667
675
|
printValue({ status: "success", command: `rspeedy ${subcommand}` }, {
|
|
@@ -753,6 +761,12 @@ async function runDevice() {
|
|
|
753
761
|
quiet: json,
|
|
754
762
|
onOutput: (line) => ui.info(`│ ${line}`),
|
|
755
763
|
});
|
|
764
|
+
if (args.includes("--launch")) {
|
|
765
|
+
await launchIosSimulatorApp(root, device, artifactPath, {
|
|
766
|
+
quiet: json,
|
|
767
|
+
onEvent: (line) => ui.info(`│ ${line}`),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
756
770
|
}
|
|
757
771
|
else {
|
|
758
772
|
assert(device !== "booted", "IOS_DEVICE_REQUIRED", "A physical iOS install requires --device <device-identifier>; use --simulator for a booted simulator.");
|
|
@@ -1228,6 +1242,7 @@ async function main() {
|
|
|
1228
1242
|
const result = await initializeIosHost(root, {
|
|
1229
1243
|
bundleIdentifier,
|
|
1230
1244
|
appName: basename(root),
|
|
1245
|
+
appIcon: flag("--icon") ?? undefined,
|
|
1231
1246
|
});
|
|
1232
1247
|
ui.success(`iOS host created: ${result.directory}`);
|
|
1233
1248
|
printValue({
|
|
@@ -1671,6 +1686,9 @@ async function main() {
|
|
|
1671
1686
|
const config = await loadConfig(root);
|
|
1672
1687
|
const profile = flag("--profile", simulator ? "simulator" : "production");
|
|
1673
1688
|
const simulatorDevice = flag("--device") ?? undefined;
|
|
1689
|
+
const simulatorAutostart = simulator &&
|
|
1690
|
+
!args.includes("--no-autostart") &&
|
|
1691
|
+
(args.includes("--autostart") || (!json && ui.interactive));
|
|
1674
1692
|
const wait = !args.includes("--no-wait");
|
|
1675
1693
|
const local = args.includes("--local");
|
|
1676
1694
|
const platforms = buildAll
|
|
@@ -1692,6 +1710,7 @@ async function main() {
|
|
|
1692
1710
|
local,
|
|
1693
1711
|
simulator,
|
|
1694
1712
|
simulatorDevice,
|
|
1713
|
+
simulatorAutostart,
|
|
1695
1714
|
state,
|
|
1696
1715
|
repository,
|
|
1697
1716
|
builds,
|
|
@@ -1723,6 +1742,7 @@ async function main() {
|
|
|
1723
1742
|
local,
|
|
1724
1743
|
simulator,
|
|
1725
1744
|
simulatorDevice,
|
|
1745
|
+
simulatorAutostart,
|
|
1726
1746
|
state,
|
|
1727
1747
|
repository,
|
|
1728
1748
|
builds,
|
package/dist/ios-build.d.ts
CHANGED
|
@@ -6,11 +6,32 @@ interface IosBuildOptions {
|
|
|
6
6
|
uploadArtifacts?: boolean;
|
|
7
7
|
simulator?: boolean;
|
|
8
8
|
simulatorDevice?: string;
|
|
9
|
+
simulatorAutostart?: boolean;
|
|
9
10
|
skipBundleBuild?: boolean;
|
|
10
11
|
quiet?: boolean;
|
|
11
12
|
onEvent?: (message: string) => void;
|
|
12
13
|
onProgress?: (value?: number, label?: string) => void;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Rspeedy leaves external resources beside the Lynx bundle. Xcode does not
|
|
17
|
+
* know about those generated files, so copy the complete output into the
|
|
18
|
+
* final app bundle after the native host has been compiled. This also makes
|
|
19
|
+
* older LynxShip-generated iOS hosts behave correctly without editing their
|
|
20
|
+
* Xcode project files.
|
|
21
|
+
*/
|
|
22
|
+
export declare function syncIosRuntimeResources(root: string, appBundle: string): Promise<string[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Apply a project-owned 1024x1024 PNG to the generated or existing Xcode
|
|
25
|
+
* AppIcon set. A missing icon is reported as a warning by the caller rather
|
|
26
|
+
* than silently inventing product branding.
|
|
27
|
+
*/
|
|
28
|
+
export declare function prepareIosAppIcon(root: string, configured?: string, options?: {
|
|
29
|
+
allowFallback?: boolean;
|
|
30
|
+
}): Promise<string | undefined>;
|
|
31
|
+
export declare function launchIosSimulatorApp(root: string, device: string, appBundle: string, options?: {
|
|
32
|
+
quiet?: boolean;
|
|
33
|
+
onEvent?: (message: string) => void;
|
|
34
|
+
}): Promise<void>;
|
|
14
35
|
export declare function hasIosHost(root: string, profile?: BuildProfile): boolean;
|
|
15
36
|
export declare function runRealIosBuild(job: BuildJob, options: IosBuildOptions): Promise<BuildJob>;
|
|
16
37
|
export {};
|
package/dist/ios-build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"AAYA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAyHD;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBnB;AAoED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACxC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuD7B;AAgCD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC,GACL,OAAO,CAAC,IAAI,CAAC,CAcf;AAkND,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAcxE;AA+DD,wBAAsB,eAAe,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAqNnB"}
|
package/dist/ios-build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { copyFile, cp, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { assert, sha256 } from "@lynxship/contracts";
|
|
@@ -56,6 +56,159 @@ async function findSimulatorApp(root, derivedData, configuration, scheme) {
|
|
|
56
56
|
assert(app, "IOS_SIMULATOR_ARTIFACT_MISSING", `Xcode did not produce a Simulator .app under ${products}`);
|
|
57
57
|
return join(products, app.name);
|
|
58
58
|
}
|
|
59
|
+
async function findArchiveApp(archivePath) {
|
|
60
|
+
const products = join(archivePath, "Products", "Applications");
|
|
61
|
+
const entries = await readdir(products, { withFileTypes: true }).catch(() => []);
|
|
62
|
+
const app = entries.find((entry) => entry.isDirectory() && entry.name.endsWith(".app"));
|
|
63
|
+
assert(app, "IOS_ARCHIVE_ARTIFACT_MISSING", `Xcode did not produce an app bundle under ${products}`);
|
|
64
|
+
return join(products, app.name);
|
|
65
|
+
}
|
|
66
|
+
async function copyIosOutput(source, target) {
|
|
67
|
+
if (!existsSync(source))
|
|
68
|
+
return false;
|
|
69
|
+
await rm(target, { recursive: true, force: true });
|
|
70
|
+
await cp(source, target, { recursive: true, force: true });
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rspeedy leaves external resources beside the Lynx bundle. Xcode does not
|
|
75
|
+
* know about those generated files, so copy the complete output into the
|
|
76
|
+
* final app bundle after the native host has been compiled. This also makes
|
|
77
|
+
* older LynxShip-generated iOS hosts behave correctly without editing their
|
|
78
|
+
* Xcode project files.
|
|
79
|
+
*/
|
|
80
|
+
export async function syncIosRuntimeResources(root, appBundle) {
|
|
81
|
+
const distRoot = join(root, "dist");
|
|
82
|
+
const entries = await readdir(distRoot, { withFileTypes: true }).catch(() => []);
|
|
83
|
+
const bundles = entries
|
|
84
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".lynx.bundle"))
|
|
85
|
+
.map((entry) => entry.name);
|
|
86
|
+
assert(bundles.length > 0, "LYNX_BUNDLE_MISSING", `No .lynx.bundle was found in ${distRoot}. Check the Rspeedy output configuration.`);
|
|
87
|
+
const copied = [];
|
|
88
|
+
for (const name of [...bundles, "async", "static"]) {
|
|
89
|
+
if (await copyIosOutput(join(distRoot, name), join(appBundle, name)))
|
|
90
|
+
copied.push(name);
|
|
91
|
+
}
|
|
92
|
+
return copied;
|
|
93
|
+
}
|
|
94
|
+
async function findAppIconSet(directory) {
|
|
95
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const path = join(directory, entry.name);
|
|
98
|
+
if (entry.isDirectory() && entry.name === "AppIcon.appiconset") {
|
|
99
|
+
if (existsSync(join(path, "Contents.json")))
|
|
100
|
+
return path;
|
|
101
|
+
}
|
|
102
|
+
if (entry.isDirectory() &&
|
|
103
|
+
!entry.name.endsWith(".xcodeproj") &&
|
|
104
|
+
!entry.name.endsWith(".xcworkspace")) {
|
|
105
|
+
const result = await findAppIconSet(path);
|
|
106
|
+
if (result)
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
function pngDimensions(content) {
|
|
113
|
+
assert(content.length >= 24 &&
|
|
114
|
+
content.subarray(0, 8).toString("hex") === "89504e470d0a1a0a", "IOS_APP_ICON_INVALID", "The iOS app icon must be a valid PNG file.");
|
|
115
|
+
return { width: content.readUInt32BE(16), height: content.readUInt32BE(20) };
|
|
116
|
+
}
|
|
117
|
+
async function findConfiguredIosIcon(root, configured, allowFallback = false) {
|
|
118
|
+
const candidates = [
|
|
119
|
+
configured,
|
|
120
|
+
"icon.png",
|
|
121
|
+
"app-icon.png",
|
|
122
|
+
"assets/icon.png",
|
|
123
|
+
"assets/app-icon.png",
|
|
124
|
+
"src/assets/icon.png",
|
|
125
|
+
"src/assets/app-icon.png",
|
|
126
|
+
"public/icon.png",
|
|
127
|
+
].filter((value) => Boolean(value));
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const path = resolve(root, candidate);
|
|
130
|
+
if (existsSync(path))
|
|
131
|
+
return { path, fallback: false };
|
|
132
|
+
}
|
|
133
|
+
if (allowFallback) {
|
|
134
|
+
const staticImages = join(root, "dist", "static", "image");
|
|
135
|
+
const entries = await readdir(staticImages, { withFileTypes: true }).catch(() => []);
|
|
136
|
+
const logo = entries.find((entry) => entry.isFile() &&
|
|
137
|
+
entry.name.endsWith(".png") &&
|
|
138
|
+
/(?:lynx|logo|icon)/i.test(entry.name));
|
|
139
|
+
if (logo)
|
|
140
|
+
return { path: join(staticImages, logo.name), fallback: true };
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Apply a project-owned 1024x1024 PNG to the generated or existing Xcode
|
|
146
|
+
* AppIcon set. A missing icon is reported as a warning by the caller rather
|
|
147
|
+
* than silently inventing product branding.
|
|
148
|
+
*/
|
|
149
|
+
export async function prepareIosAppIcon(root, configured, options = {}) {
|
|
150
|
+
const source = await findConfiguredIosIcon(root, configured, options.allowFallback);
|
|
151
|
+
const iconSet = await findAppIconSet(join(root, "ios"));
|
|
152
|
+
if (!source || !iconSet)
|
|
153
|
+
return undefined;
|
|
154
|
+
const dimensions = pngDimensions(await readFile(source.path));
|
|
155
|
+
const filename = "AppIcon.png";
|
|
156
|
+
const destination = join(iconSet, filename);
|
|
157
|
+
if (dimensions.width === 1024 && dimensions.height === 1024) {
|
|
158
|
+
await copyFile(source.path, destination);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
assert(source.fallback &&
|
|
162
|
+
dimensions.width === dimensions.height &&
|
|
163
|
+
dimensions.width >= 512, "IOS_APP_ICON_INVALID", `The iOS app icon must be exactly 1024x1024 PNG; received ${dimensions.width}x${dimensions.height} from ${source.path}.`);
|
|
164
|
+
assert(commandExists("sips"), "IOS_SIPS_REQUIRED", "The fallback Simulator icon needs Apple's sips tool. Install Xcode command-line tools, or provide a 1024x1024 icon.png.");
|
|
165
|
+
await runProcess("sips", ["-z", "1024", "1024", source.path, "--out", destination], {
|
|
166
|
+
cwd: root,
|
|
167
|
+
quiet: true,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
await writeFile(join(iconSet, "Contents.json"), `${JSON.stringify({
|
|
171
|
+
images: [
|
|
172
|
+
{
|
|
173
|
+
filename,
|
|
174
|
+
idiom: "universal",
|
|
175
|
+
platform: "ios",
|
|
176
|
+
size: "1024x1024",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
info: { author: "xcode", version: 1 },
|
|
180
|
+
}, null, 2)}\n`, "utf8");
|
|
181
|
+
return source.path;
|
|
182
|
+
}
|
|
183
|
+
async function appBundleIdentifier(root, appBundle) {
|
|
184
|
+
assert(commandExists("plutil"), "IOS_PLUTIL_REQUIRED", "plutil was not found. Install Xcode command-line tools before launching the Simulator app.");
|
|
185
|
+
const result = await captureProcess("plutil", [
|
|
186
|
+
"-extract",
|
|
187
|
+
"CFBundleIdentifier",
|
|
188
|
+
"raw",
|
|
189
|
+
"-o",
|
|
190
|
+
"-",
|
|
191
|
+
join(appBundle, "Info.plist"),
|
|
192
|
+
], { cwd: root });
|
|
193
|
+
const identifier = result.stdout.trim();
|
|
194
|
+
assert(result.code === 0 && identifier, "IOS_BUNDLE_IDENTIFIER_MISSING", `Could not read CFBundleIdentifier from ${join(appBundle, "Info.plist")}.`);
|
|
195
|
+
return identifier;
|
|
196
|
+
}
|
|
197
|
+
export async function launchIosSimulatorApp(root, device, appBundle, options = {}) {
|
|
198
|
+
const identifier = await appBundleIdentifier(root, appBundle);
|
|
199
|
+
options.onEvent?.("Opening iOS Simulator…");
|
|
200
|
+
await runProcess("open", ["-a", "Simulator"], {
|
|
201
|
+
cwd: root,
|
|
202
|
+
quiet: options.quiet,
|
|
203
|
+
onOutput: options.onEvent,
|
|
204
|
+
});
|
|
205
|
+
options.onEvent?.(`Launching ${identifier} in iOS Simulator…`);
|
|
206
|
+
await runProcess("xcrun", ["simctl", "launch", device, identifier], {
|
|
207
|
+
cwd: root,
|
|
208
|
+
quiet: options.quiet,
|
|
209
|
+
onOutput: options.onEvent,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
59
212
|
async function hashDirectory(directory) {
|
|
60
213
|
const hash = createHash("sha256");
|
|
61
214
|
let size = 0;
|
|
@@ -121,6 +274,13 @@ async function runRealIosSimulatorBuild(job, options) {
|
|
|
121
274
|
onOutput: options.onEvent,
|
|
122
275
|
});
|
|
123
276
|
}
|
|
277
|
+
const icon = await prepareIosAppIcon(options.root, ios.appIcon, {
|
|
278
|
+
allowFallback: true,
|
|
279
|
+
});
|
|
280
|
+
if (icon)
|
|
281
|
+
step(`Using iOS app icon: ${icon}`, 9);
|
|
282
|
+
else
|
|
283
|
+
options.onEvent?.("No 1024x1024 PNG app icon was found; configure ios.appIcon or add icon.png before distribution.");
|
|
124
284
|
transitionBuild(job, "queued", "iOS Simulator build queued locally");
|
|
125
285
|
transitionBuild(job, "provisioning", "iOS Simulator destination selected");
|
|
126
286
|
transitionBuild(job, "installing_dependencies", "iOS Simulator dependencies ready");
|
|
@@ -145,12 +305,26 @@ async function runRealIosSimulatorBuild(job, options) {
|
|
|
145
305
|
"build",
|
|
146
306
|
], { cwd: options.root, quiet: options.quiet, onOutput: options.onEvent });
|
|
147
307
|
const appPath = await findSimulatorApp(options.root, derivedData, configuration, ios.scheme);
|
|
308
|
+
const copiedResources = await syncIosRuntimeResources(options.root, appPath);
|
|
309
|
+
for (const name of copiedResources)
|
|
310
|
+
options.onEvent?.(`Packaged ${name} in the iOS Simulator app`);
|
|
148
311
|
step("Installing .app in iOS Simulator…", 75);
|
|
149
312
|
await runProcess("xcrun", ["simctl", "install", device, appPath], {
|
|
150
313
|
cwd: options.root,
|
|
151
314
|
quiet: options.quiet,
|
|
152
315
|
onOutput: options.onEvent,
|
|
153
316
|
});
|
|
317
|
+
if (options.simulatorAutostart) {
|
|
318
|
+
try {
|
|
319
|
+
await launchIosSimulatorApp(options.root, device, appPath, {
|
|
320
|
+
quiet: options.quiet,
|
|
321
|
+
onEvent: options.onEvent,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
options.onEvent?.(`Simulator app installed but could not be launched automatically: ${error instanceof Error ? error.message : String(error)}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
154
328
|
const artifact = await hashDirectory(appPath);
|
|
155
329
|
const artifactName = nativeArtifactName("app");
|
|
156
330
|
job.attempts += 1;
|
|
@@ -290,6 +464,11 @@ export async function runRealIosBuild(job, options) {
|
|
|
290
464
|
onOutput: options.onEvent,
|
|
291
465
|
});
|
|
292
466
|
}
|
|
467
|
+
const icon = await prepareIosAppIcon(options.root, ios.appIcon);
|
|
468
|
+
if (icon)
|
|
469
|
+
step(`Using iOS app icon: ${icon}`, 9);
|
|
470
|
+
else
|
|
471
|
+
options.onEvent?.("No 1024x1024 PNG app icon was found; configure ios.appIcon or add icon.png before distribution.");
|
|
293
472
|
step("Preparing Xcode archive…", 10);
|
|
294
473
|
await runProcess("xcodebuild", [
|
|
295
474
|
projectFlag,
|
|
@@ -304,6 +483,10 @@ export async function runRealIosBuild(job, options) {
|
|
|
304
483
|
archivePath,
|
|
305
484
|
"archive",
|
|
306
485
|
], { cwd: options.root, quiet: options.quiet, onOutput: options.onEvent });
|
|
486
|
+
const archivedApp = await findArchiveApp(archivePath);
|
|
487
|
+
const copiedResources = await syncIosRuntimeResources(options.root, archivedApp);
|
|
488
|
+
for (const name of copiedResources)
|
|
489
|
+
options.onEvent?.(`Packaged ${name} in the iOS archive`);
|
|
307
490
|
transitionBuild(job, "queued", "iOS build queued locally");
|
|
308
491
|
transitionBuild(job, "provisioning", "Xcode signing configuration selected");
|
|
309
492
|
step("Exporting signed IPA…", 55);
|
package/dist/ios-host.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface IosHostOptions {
|
|
2
2
|
bundleIdentifier: string;
|
|
3
3
|
appName: string;
|
|
4
|
+
appIcon?: string;
|
|
4
5
|
}
|
|
5
6
|
export interface IosHostResult {
|
|
6
7
|
directory: string;
|
|
@@ -10,6 +11,7 @@ export interface IosHostResult {
|
|
|
10
11
|
scheme: string;
|
|
11
12
|
exportOptionsPlist: string;
|
|
12
13
|
bundleScript: string;
|
|
14
|
+
appIcon?: string;
|
|
13
15
|
configUpdated: boolean;
|
|
14
16
|
}
|
|
15
17
|
export declare function initializeIosHost(root: string, options: IosHostOptions): Promise<IosHostResult>;
|
package/dist/ios-host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios-host.d.ts","sourceRoot":"","sources":["../src/ios-host.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ios-host.d.ts","sourceRoot":"","sources":["../src/ios-host.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;CACxB;AAqED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CA2CxB;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE"}
|
package/dist/ios-host.js
CHANGED
|
@@ -51,6 +51,7 @@ async function updateProjectConfig(root, result) {
|
|
|
51
51
|
configuration: "Release",
|
|
52
52
|
exportOptionsPlist: "ios/ExportOptions.plist",
|
|
53
53
|
bundleScript: "ios/sync-bundle.mjs",
|
|
54
|
+
...(result.appIcon ? { appIcon: result.appIcon } : {}),
|
|
54
55
|
};
|
|
55
56
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
56
57
|
return true;
|
|
@@ -81,6 +82,7 @@ export async function initializeIosHost(root, options) {
|
|
|
81
82
|
scheme,
|
|
82
83
|
exportOptionsPlist: "ios/ExportOptions.plist",
|
|
83
84
|
bundleScript: "ios/sync-bundle.mjs",
|
|
85
|
+
...(options.appIcon ? { appIcon: options.appIcon } : {}),
|
|
84
86
|
};
|
|
85
87
|
const configUpdated = await updateProjectConfig(root, result);
|
|
86
88
|
return { ...result, configUpdated };
|
package/dist/ui/components.d.ts
CHANGED
|
@@ -35,5 +35,5 @@ export declare const pill: {
|
|
|
35
35
|
export declare function finalLine(message: string, success?: boolean): void;
|
|
36
36
|
export declare function nextSteps(commands: string[], note?: string, environment?: string): void;
|
|
37
37
|
export declare function downloadArtifact(url: string, expiresAt?: string, enabled?: boolean): void;
|
|
38
|
-
export declare function devServerQr(url: string, enabled?: boolean): void
|
|
38
|
+
export declare function devServerQr(url: string, enabled?: boolean): Promise<void>;
|
|
39
39
|
//# sourceMappingURL=components.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAK7C,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,eAAO,MAAM,GAAG;qBACG,MAAM,GAAG,IAAI;kBAGhB,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;kBAGd,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;CAG7B,CAAC;AAgBF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA6C9D;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,IAAI,IAAI,CAAC;CACd;AA6CD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoBnD;AA6BD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf,cAAc,CAuEhB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CA2BlE;AAYD,eAAO,MAAM,IAAI;oBACC,MAAM;iBACT,MAAM;iBACN,MAAM;kBACL,MAAM;CACrB,CAAC;AAEF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI,CAE/D;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAAE,EAClB,IAAI,CAAC,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAQN;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,UAAO,GACb,IAAI,CAQN;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB5E"}
|
package/dist/ui/components.js
CHANGED
|
@@ -5,6 +5,7 @@ import ora from "ora";
|
|
|
5
5
|
import qrcode from "qrcode-terminal";
|
|
6
6
|
import { c, colorsEnabled } from "./state.js";
|
|
7
7
|
import { supportsUnicode, terminalWidth } from "./terminal.js";
|
|
8
|
+
import { renderTerminalQr } from "./qr.js";
|
|
8
9
|
export function sectionHeader(title) {
|
|
9
10
|
console.log(`\n${c.brandBold(`◆ LynxShip — ${title}`)}`);
|
|
10
11
|
}
|
|
@@ -288,10 +289,25 @@ export function downloadArtifact(url, expiresAt, enabled = true) {
|
|
|
288
289
|
if (expiresAt)
|
|
289
290
|
console.log(` ${c.muted(`Link expires at ${expiresAt}`)}`);
|
|
290
291
|
}
|
|
291
|
-
export function devServerQr(url, enabled = true) {
|
|
292
|
+
export async function devServerQr(url, enabled = true) {
|
|
292
293
|
if (!enabled)
|
|
293
294
|
return;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
const rail = c.teal("│");
|
|
296
|
+
const printRail = (value = "") => console.log(` ${rail}${value}`);
|
|
297
|
+
printRail();
|
|
298
|
+
printRail(` ${c.brandBold("Lynx Explorer")}`);
|
|
299
|
+
if (colorsEnabled && supportsUnicode()) {
|
|
300
|
+
(await renderTerminalQr(url))
|
|
301
|
+
.split("\n")
|
|
302
|
+
.forEach((line) => printRail(` ${line}`));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
qrcode.generate(url, { small: true }, (code) => {
|
|
306
|
+
code.split("\n").forEach((line) => printRail(` ${line}`));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
printRail();
|
|
310
|
+
printRail(` ${c.teal("URL")} ${url}`);
|
|
311
|
+
printRail();
|
|
312
|
+
console.log();
|
|
297
313
|
}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare class CliUi {
|
|
|
19
19
|
spinner(text: string): SpinnerHandle;
|
|
20
20
|
done(message: string, success?: boolean): void;
|
|
21
21
|
downloadArtifact(url: string, expiresAt?: string): void;
|
|
22
|
-
devServerQr(url: string): void
|
|
22
|
+
devServerQr(url: string): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
export declare function createCliUi(args: string[]): CliUi;
|
|
25
25
|
export { terminalOptions };
|
package/dist/ui/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;gBAElB,IAAI,EAAE,MAAM,EAAE;IAU1B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAKtC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI5C,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAKzC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAIpC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI;IAI3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;gBAElB,IAAI,EAAE,MAAM,EAAE;IAU1B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAKtC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI5C,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAKzC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAIpC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI;IAI3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIjD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS9C;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAEjD;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/ui/index.js
CHANGED
|
@@ -66,8 +66,11 @@ export class CliUi {
|
|
|
66
66
|
downloadArtifact(url, expiresAt) {
|
|
67
67
|
downloadArtifact(url, expiresAt, this.interactive);
|
|
68
68
|
}
|
|
69
|
-
devServerQr(url) {
|
|
70
|
-
|
|
69
|
+
async devServerQr(url) {
|
|
70
|
+
// Rspeedy suppresses its own QR when its stdout is piped. LynxShip
|
|
71
|
+
// captures that output, so render the fallback unless the caller asked
|
|
72
|
+
// for machine JSON, quiet output, or an explicitly non-interactive run.
|
|
73
|
+
await devServerQr(url, !this.options.json && !this.options.quiet && !this.options.nonInteractive);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
export function createCliUi(args) {
|
package/dist/ui/qr.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const LYNXSHIP_QR_STYLE: {
|
|
2
|
+
readonly type: "svg";
|
|
3
|
+
readonly width: 300;
|
|
4
|
+
readonly height: 300;
|
|
5
|
+
readonly margin: 0;
|
|
6
|
+
readonly qrOptions: {
|
|
7
|
+
readonly errorCorrectionLevel: "H";
|
|
8
|
+
};
|
|
9
|
+
readonly dotsOptions: {
|
|
10
|
+
readonly type: "dots";
|
|
11
|
+
readonly gradient: {
|
|
12
|
+
readonly type: "linear";
|
|
13
|
+
readonly rotation: number;
|
|
14
|
+
readonly colorStops: readonly [{
|
|
15
|
+
readonly offset: 0;
|
|
16
|
+
readonly color: "#ff6b9d";
|
|
17
|
+
}, {
|
|
18
|
+
readonly offset: 1;
|
|
19
|
+
readonly color: "#45b7d1";
|
|
20
|
+
}];
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
readonly cornersSquareOptions: {
|
|
24
|
+
readonly type: "dot";
|
|
25
|
+
readonly gradient: {
|
|
26
|
+
readonly type: "linear";
|
|
27
|
+
readonly rotation: number;
|
|
28
|
+
readonly colorStops: readonly [{
|
|
29
|
+
readonly offset: 0;
|
|
30
|
+
readonly color: "#ff6b9d";
|
|
31
|
+
}, {
|
|
32
|
+
readonly offset: 1;
|
|
33
|
+
readonly color: "#45b7d1";
|
|
34
|
+
}];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
readonly cornersDotOptions: {
|
|
38
|
+
readonly type: "dot";
|
|
39
|
+
readonly gradient: {
|
|
40
|
+
readonly type: "linear";
|
|
41
|
+
readonly rotation: number;
|
|
42
|
+
readonly colorStops: readonly [{
|
|
43
|
+
readonly offset: 0;
|
|
44
|
+
readonly color: "#ff6b9d";
|
|
45
|
+
}, {
|
|
46
|
+
readonly offset: 1;
|
|
47
|
+
readonly color: "#45b7d1";
|
|
48
|
+
}];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
readonly backgroundOptions: {
|
|
52
|
+
readonly color: "#FFFFFF";
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Generate the QR with qr-code-styling itself, exactly like the WISA renderer.
|
|
57
|
+
* The SVG is then rasterized only to make the library's real rounded shapes
|
|
58
|
+
* displayable in a normal terminal; no QR modules are recreated by LynxShip.
|
|
59
|
+
*/
|
|
60
|
+
export declare function renderTerminalQr(data: string): Promise<string>;
|
|
61
|
+
//# sourceMappingURL=qr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr.d.ts","sourceRoot":"","sources":["../../src/ui/qr.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCpB,CAAC;AAyLX;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBpE"}
|
package/dist/ui/qr.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { LYNX_BRAND } from "./colors.js";
|
|
2
|
+
import { terminalWidth } from "./terminal.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
export const LYNXSHIP_QR_STYLE = {
|
|
5
|
+
type: "svg",
|
|
6
|
+
width: 300,
|
|
7
|
+
height: 300,
|
|
8
|
+
margin: 0,
|
|
9
|
+
qrOptions: { errorCorrectionLevel: "H" },
|
|
10
|
+
dotsOptions: {
|
|
11
|
+
type: "dots",
|
|
12
|
+
gradient: {
|
|
13
|
+
type: "linear",
|
|
14
|
+
rotation: (50 * Math.PI) / 180,
|
|
15
|
+
colorStops: [
|
|
16
|
+
{ offset: 0, color: LYNX_BRAND.pink },
|
|
17
|
+
{ offset: 1, color: LYNX_BRAND.cyan },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
cornersSquareOptions: {
|
|
22
|
+
type: "dot",
|
|
23
|
+
gradient: {
|
|
24
|
+
type: "linear",
|
|
25
|
+
rotation: (50 * Math.PI) / 180,
|
|
26
|
+
colorStops: [
|
|
27
|
+
{ offset: 0, color: LYNX_BRAND.pink },
|
|
28
|
+
{ offset: 1, color: LYNX_BRAND.cyan },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
cornersDotOptions: {
|
|
33
|
+
type: "dot",
|
|
34
|
+
gradient: {
|
|
35
|
+
type: "linear",
|
|
36
|
+
rotation: (50 * Math.PI) / 180,
|
|
37
|
+
colorStops: [
|
|
38
|
+
{ offset: 0, color: LYNX_BRAND.pink },
|
|
39
|
+
{ offset: 1, color: LYNX_BRAND.cyan },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
backgroundOptions: { color: "#FFFFFF" },
|
|
44
|
+
};
|
|
45
|
+
async function loadDependencies() {
|
|
46
|
+
const require = createRequire(import.meta.url);
|
|
47
|
+
const qrModule = require("qr-code-styling");
|
|
48
|
+
const jsdomModule = require("jsdom");
|
|
49
|
+
const resvgModule = require("@resvg/resvg-js");
|
|
50
|
+
const pngModule = require("pngjs");
|
|
51
|
+
return {
|
|
52
|
+
QRCodeStyling: "default" in qrModule && qrModule.default
|
|
53
|
+
? qrModule.default
|
|
54
|
+
: qrModule,
|
|
55
|
+
JSDOM: jsdomModule.JSDOM,
|
|
56
|
+
Resvg: resvgModule.Resvg,
|
|
57
|
+
PNG: pngModule.PNG,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function ansiColor(channel, red, green, blue) {
|
|
61
|
+
return "\u001b[" + channel + ";2;" + red + ";" + green + ";" + blue + "m";
|
|
62
|
+
}
|
|
63
|
+
function pixelAt(image, x, y) {
|
|
64
|
+
const offset = (y * image.width + x) * 4;
|
|
65
|
+
return [
|
|
66
|
+
image.data[offset] ?? 255,
|
|
67
|
+
image.data[offset + 1] ?? 255,
|
|
68
|
+
image.data[offset + 2] ?? 255,
|
|
69
|
+
image.data[offset + 3] ?? 255,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
function isBackground([red, green, blue, alpha]) {
|
|
73
|
+
return alpha < 16 || (red > 248 && green > 248 && blue > 248);
|
|
74
|
+
}
|
|
75
|
+
function cropToQrContent(image) {
|
|
76
|
+
let left = image.width;
|
|
77
|
+
let top = image.height;
|
|
78
|
+
let right = -1;
|
|
79
|
+
let bottom = -1;
|
|
80
|
+
for (let y = 0; y < image.height; y += 1) {
|
|
81
|
+
for (let x = 0; x < image.width; x += 1) {
|
|
82
|
+
if (!isBackground(pixelAt(image, x, y))) {
|
|
83
|
+
left = Math.min(left, x);
|
|
84
|
+
top = Math.min(top, y);
|
|
85
|
+
right = Math.max(right, x);
|
|
86
|
+
bottom = Math.max(bottom, y);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (right < left || bottom < top)
|
|
91
|
+
return image;
|
|
92
|
+
const padding = Math.max(3, Math.round(Math.min(image.width, image.height) * 0.02));
|
|
93
|
+
const cropLeft = Math.max(0, left - padding);
|
|
94
|
+
const cropTop = Math.max(0, top - padding);
|
|
95
|
+
const cropRight = Math.min(image.width - 1, right + padding);
|
|
96
|
+
const cropBottom = Math.min(image.height - 1, bottom + padding);
|
|
97
|
+
const width = cropRight - cropLeft + 1;
|
|
98
|
+
const height = cropBottom - cropTop + 1;
|
|
99
|
+
const data = new Uint8Array(width * height * 4);
|
|
100
|
+
for (let y = 0; y < height; y += 1) {
|
|
101
|
+
for (let x = 0; x < width; x += 1) {
|
|
102
|
+
const source = ((cropTop + y) * image.width + cropLeft + x) * 4;
|
|
103
|
+
const target = (y * width + x) * 4;
|
|
104
|
+
data[target] = image.data[source] ?? 255;
|
|
105
|
+
data[target + 1] = image.data[source + 1] ?? 255;
|
|
106
|
+
data[target + 2] = image.data[source + 2] ?? 255;
|
|
107
|
+
data[target + 3] = image.data[source + 3] ?? 255;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { width, height, data };
|
|
111
|
+
}
|
|
112
|
+
function renderRasterInTerminal(image) {
|
|
113
|
+
const columns = Math.max(35, Math.min(72, terminalWidth() - 8));
|
|
114
|
+
const scale = image.width / columns;
|
|
115
|
+
const rows = Math.ceil(image.height / scale / 2);
|
|
116
|
+
const lines = [];
|
|
117
|
+
const whiteBackground = ansiColor("48", 255, 255, 255);
|
|
118
|
+
const reset = "\u001b[0m";
|
|
119
|
+
for (let row = 0; row < rows; row += 1) {
|
|
120
|
+
const topY = Math.min(image.height - 1, Math.floor(row * 2 * scale));
|
|
121
|
+
const bottomY = Math.min(image.height - 1, Math.floor((row * 2 + 1) * scale));
|
|
122
|
+
let line = whiteBackground;
|
|
123
|
+
for (let column = 0; column < columns; column += 1) {
|
|
124
|
+
const x = Math.min(image.width - 1, Math.floor(column * scale));
|
|
125
|
+
const top = pixelAt(image, x, topY);
|
|
126
|
+
const bottom = pixelAt(image, x, bottomY);
|
|
127
|
+
const topBackground = isBackground(top);
|
|
128
|
+
const bottomBackground = isBackground(bottom);
|
|
129
|
+
if (topBackground && bottomBackground) {
|
|
130
|
+
line += " ";
|
|
131
|
+
}
|
|
132
|
+
else if (!topBackground && bottomBackground) {
|
|
133
|
+
line +=
|
|
134
|
+
ansiColor("38", top[0], top[1], top[2]) +
|
|
135
|
+
ansiColor("48", 255, 255, 255) +
|
|
136
|
+
"▀";
|
|
137
|
+
}
|
|
138
|
+
else if (topBackground && !bottomBackground) {
|
|
139
|
+
line +=
|
|
140
|
+
ansiColor("38", bottom[0], bottom[1], bottom[2]) +
|
|
141
|
+
ansiColor("48", 255, 255, 255) +
|
|
142
|
+
"▄";
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
line +=
|
|
146
|
+
ansiColor("38", top[0], top[1], top[2]) +
|
|
147
|
+
ansiColor("48", bottom[0], bottom[1], bottom[2]) +
|
|
148
|
+
"▀";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
lines.push(line + reset);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate the QR with qr-code-styling itself, exactly like the WISA renderer.
|
|
157
|
+
* The SVG is then rasterized only to make the library's real rounded shapes
|
|
158
|
+
* displayable in a normal terminal; no QR modules are recreated by LynxShip.
|
|
159
|
+
*/
|
|
160
|
+
export async function renderTerminalQr(data) {
|
|
161
|
+
const { QRCodeStyling, JSDOM, Resvg, PNG } = await loadDependencies();
|
|
162
|
+
const qrCode = new QRCodeStyling({
|
|
163
|
+
jsdom: JSDOM,
|
|
164
|
+
data,
|
|
165
|
+
...LYNXSHIP_QR_STYLE,
|
|
166
|
+
});
|
|
167
|
+
const svg = await qrCode.getRawData("svg");
|
|
168
|
+
if (!svg)
|
|
169
|
+
throw new Error("qr-code-styling returned no SVG data");
|
|
170
|
+
const svgText = Buffer.isBuffer(svg)
|
|
171
|
+
? svg.toString("utf8")
|
|
172
|
+
: new TextDecoder().decode(svg);
|
|
173
|
+
// jsdom serializes SVG references as url('#id'). Normalize only those
|
|
174
|
+
// references for resvg; the SVG generated by qr-code-styling is unchanged
|
|
175
|
+
// for browsers and for the file returned by the library.
|
|
176
|
+
const rasterSvg = svgText.replaceAll("url('#", "url(#").replaceAll("')", ")");
|
|
177
|
+
const png = new Resvg(rasterSvg).render().asPng();
|
|
178
|
+
const image = cropToQrContent(PNG.sync.read(Buffer.from(png)));
|
|
179
|
+
return renderRasterInTerminal(image);
|
|
180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynxship/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Build, sign, store, submit and update LynxJS applications from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,14 +68,19 @@
|
|
|
68
68
|
"cli-progress": "3.12.0",
|
|
69
69
|
"ora": "8.2.0",
|
|
70
70
|
"qrcode-terminal": "0.12.0",
|
|
71
|
+
"qr-code-styling": "1.9.2",
|
|
72
|
+
"@resvg/resvg-js": "2.6.2",
|
|
73
|
+
"jsdom": "29.1.1",
|
|
74
|
+
"pngjs": "7.0.0",
|
|
71
75
|
"@lynxship/build-orchestrator": "0.1.0",
|
|
72
|
-
"@lynxship/contracts": "0.1.0",
|
|
73
76
|
"@lynxship/db": "0.1.0",
|
|
74
|
-
"@lynxship/
|
|
75
|
-
"@lynxship/signing": "0.1.0"
|
|
77
|
+
"@lynxship/contracts": "0.1.0",
|
|
78
|
+
"@lynxship/signing": "0.1.0",
|
|
79
|
+
"@lynxship/submit": "0.1.0"
|
|
76
80
|
},
|
|
77
81
|
"devDependencies": {
|
|
78
82
|
"@types/cli-progress": "3.11.6",
|
|
83
|
+
"@types/pngjs": "6.0.4",
|
|
79
84
|
"@types/qrcode-terminal": "0.12.2"
|
|
80
85
|
},
|
|
81
86
|
"scripts": {
|