@lynxship/cli 0.1.4 → 0.1.6
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 +48 -2
- package/dist/android-host.d.ts +12 -0
- package/dist/android-host.d.ts.map +1 -0
- package/dist/android-host.js +70 -0
- package/dist/index.js +97 -4
- package/dist/ios-build.d.ts.map +1 -1
- package/dist/ios-build.js +26 -4
- package/dist/ios-host.d.ts +17 -0
- package/dist/ios-host.d.ts.map +1 -0
- package/dist/ios-host.js +93 -0
- package/package.json +6 -5
- package/templates/android-host/app/build.gradle +38 -0
- package/templates/android-host/app/proguard-rules.pro +2 -0
- package/templates/android-host/app/src/main/AndroidManifest.xml +20 -0
- package/templates/android-host/app/src/main/java/template/LynxShipApplication.java +13 -0
- package/templates/android-host/app/src/main/java/template/MainActivity.java +19 -0
- package/templates/android-host/app/src/main/java/template/ProjectTemplateProvider.java +32 -0
- package/templates/android-host/app/src/main/res/values/strings.xml +3 -0
- package/templates/android-host/app/src/main/res/values/themes.xml +6 -0
- package/templates/android-host/build.gradle +3 -0
- package/templates/android-host/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/android-host/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/templates/android-host/gradle.properties +3 -0
- package/templates/android-host/gradlew +185 -0
- package/templates/android-host/gradlew.bat +89 -0
- package/templates/android-host/settings.gradle +20 -0
- package/templates/ios-host/ExportOptions.plist +16 -0
- package/templates/ios-host/Podfile +42 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/AppDelegate.swift +11 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/Contents.json +6 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/LaunchScreen.storyboard +25 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/Main.storyboard +24 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/DemoLynxProvider.swift +18 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Hello-Lynx-Bridging-Header.h +4 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Info.plist +25 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/SceneDelegate.swift +49 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/ViewController.swift +22 -0
- package/templates/ios-host/__IOS_TARGET_NAME__.xcodeproj/project.pbxproj +452 -0
- package/templates/ios-host/sync-bundle.mjs +11 -0
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ unverified artifact.
|
|
|
117
117
|
```text
|
|
118
118
|
init Initialize or link a project
|
|
119
119
|
doctor Check the local toolchain and project
|
|
120
|
-
dev Run
|
|
120
|
+
dev Run Rspeedy dev with Lynx Explorer QR/HMR
|
|
121
121
|
preview Preview the production bundle locally
|
|
122
122
|
build create Build, sign and upload an artifact
|
|
123
123
|
build list List build jobs
|
|
@@ -133,7 +133,8 @@ logs Stream native logs
|
|
|
133
133
|
autolink check Check Lynx native-library wiring
|
|
134
134
|
autolink codegen Run native-module codegen
|
|
135
135
|
ota doctor Check native OTA host integration
|
|
136
|
-
storage configure
|
|
136
|
+
storage configure Configure Cloudflare R2
|
|
137
|
+
android host init Create a minimal official Lynx Android host
|
|
137
138
|
android configure Configure Android signing
|
|
138
139
|
store configure Configure store submission credentials
|
|
139
140
|
```
|
|
@@ -170,6 +171,51 @@ command-line tools with `sdkmanager`, accept the required licenses, set
|
|
|
170
171
|
`android/gradlew` is executable. macOS can build both Android and iOS; Windows
|
|
171
172
|
and Linux can build Android only.
|
|
172
173
|
|
|
174
|
+
## Pure Lynx projects and Lynx Explorer
|
|
175
|
+
|
|
176
|
+
A standard Rspeedy project can be developed without a native Android host:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
lynxship dev --project-dir ./my-lynx-app
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Rspeedy serves the development bundle and prints the QR code. Scan it with the
|
|
183
|
+
official Lynx Explorer app; edits to the Lynx source are then reflected live.
|
|
184
|
+
This is the supported path for projects containing `src/` and `lynx.config.*`
|
|
185
|
+
but no `android/` directory.
|
|
186
|
+
|
|
187
|
+
Production APK/AAB builds require a native Android host. The host is the
|
|
188
|
+
Android application that initializes Lynx, creates `LynxView`, loads the bundle
|
|
189
|
+
and contains the Gradle wrapper. `lynxship build` detects that requirement and
|
|
190
|
+
fails clearly when `android/gradlew` is absent; `--local` only tests LynxShip's
|
|
191
|
+
contract state machine and never fabricates an APK.
|
|
192
|
+
|
|
193
|
+
To create a minimal host for a pure project:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
lynxship android host init --application-id com.example.myapp
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This command never overwrites an existing `android/` directory. It creates a
|
|
200
|
+
real Gradle application with the official Lynx Android dependencies,
|
|
201
|
+
`LynxEnv`, `LynxView`, a bundle loader and a Gradle wrapper. Replace the example
|
|
202
|
+
application ID before a store release and add any project-specific native
|
|
203
|
+
modules, permissions, services and OTA integration explicitly.
|
|
204
|
+
|
|
205
|
+
For a pure project that targets iOS, create the native Xcode/CocoaPods host
|
|
206
|
+
with:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
lynxship ios host init --bundle-identifier com.example.myapp
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The command refuses to overwrite an existing `ios/` directory and creates a
|
|
213
|
+
Swift host based on Lynx's official integration shape: `LynxEnv`, `LynxView`,
|
|
214
|
+
`LynxTemplateProvider`, `Podfile`, `ExportOptions.plist` and a bundle sync
|
|
215
|
+
script. On macOS, `lynxship build --platform ios` installs CocoaPods before
|
|
216
|
+
archiving. Xcode, CocoaPods and real Apple signing credentials are still
|
|
217
|
+
required for a signed IPA; the CLI never fabricates them.
|
|
218
|
+
|
|
173
219
|
## Package layout
|
|
174
220
|
|
|
175
221
|
The CLI is backed by the public `@lynxship/*` runtime packages in this
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface AndroidHostOptions {
|
|
2
|
+
applicationId: string;
|
|
3
|
+
appName: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AndroidHostResult {
|
|
6
|
+
directory: string;
|
|
7
|
+
applicationId: string;
|
|
8
|
+
packageName: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function initializeAndroidHost(root: string, options: AndroidHostOptions): Promise<AndroidHostResult>;
|
|
11
|
+
export declare function suggestedAndroidApplicationId(root: string): string;
|
|
12
|
+
//# sourceMappingURL=android-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"android-host.d.ts","sourceRoot":"","sources":["../src/android-host.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AA2BD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CA6D5B;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKlE"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { access, chmod, cp, mkdir, readFile, rename, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { assert } from "@lynxship/contracts";
|
|
5
|
+
const templateRoot = fileURLToPath(new URL("../templates/android-host/", import.meta.url));
|
|
6
|
+
function packagePath(packageName) {
|
|
7
|
+
return packageName.split(".").join("/");
|
|
8
|
+
}
|
|
9
|
+
function validateApplicationId(applicationId) {
|
|
10
|
+
assert(/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId), "ANDROID_APPLICATION_ID_INVALID", "Android application ID must contain at least two dot-separated Java package segments, for example com.example.myapp.");
|
|
11
|
+
}
|
|
12
|
+
async function exists(path) {
|
|
13
|
+
try {
|
|
14
|
+
await access(path);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function safeAppName(value) {
|
|
22
|
+
return value.replace(/[^a-zA-Z0-9 ._-]/g, "").trim() || "Lynx App";
|
|
23
|
+
}
|
|
24
|
+
export async function initializeAndroidHost(root, options) {
|
|
25
|
+
validateApplicationId(options.applicationId);
|
|
26
|
+
const android = join(root, "android");
|
|
27
|
+
assert(!(await exists(android)), "ANDROID_HOST_EXISTS", `The Android host already exists at ${android}. LynxShip will not overwrite it.`);
|
|
28
|
+
const packageName = options.applicationId;
|
|
29
|
+
const packageDirectory = join(android, "app", "src", "main", "java", packagePath(packageName));
|
|
30
|
+
await cp(templateRoot, android, { recursive: true, force: false });
|
|
31
|
+
await mkdir(dirname(packageDirectory), { recursive: true });
|
|
32
|
+
await rename(join(android, "app", "src", "main", "java", "template"), packageDirectory);
|
|
33
|
+
const replacements = {
|
|
34
|
+
__APPLICATION_ID__: options.applicationId,
|
|
35
|
+
__PACKAGE_NAME__: packageName,
|
|
36
|
+
__APP_NAME__: safeAppName(options.appName),
|
|
37
|
+
};
|
|
38
|
+
const textFiles = [
|
|
39
|
+
"build.gradle",
|
|
40
|
+
"settings.gradle",
|
|
41
|
+
"gradle.properties",
|
|
42
|
+
"app/build.gradle",
|
|
43
|
+
"app/src/main/AndroidManifest.xml",
|
|
44
|
+
"app/src/main/res/values/strings.xml",
|
|
45
|
+
"app/src/main/res/values/themes.xml",
|
|
46
|
+
"app/src/main/java/__PACKAGE_PATH__/LynxShipApplication.java",
|
|
47
|
+
"app/src/main/java/__PACKAGE_PATH__/MainActivity.java",
|
|
48
|
+
"app/src/main/java/__PACKAGE_PATH__/ProjectTemplateProvider.java",
|
|
49
|
+
];
|
|
50
|
+
for (const relativeFile of textFiles) {
|
|
51
|
+
const target = join(android, relativeFile.replace("__PACKAGE_PATH__", packagePath(packageName)));
|
|
52
|
+
let content = await readFile(target, "utf8");
|
|
53
|
+
for (const [placeholder, value] of Object.entries(replacements))
|
|
54
|
+
content = content.replaceAll(placeholder, value);
|
|
55
|
+
await writeFile(target, content, "utf8");
|
|
56
|
+
}
|
|
57
|
+
if (process.platform !== "win32")
|
|
58
|
+
await chmod(join(android, "gradlew"), 0o755);
|
|
59
|
+
return {
|
|
60
|
+
directory: android,
|
|
61
|
+
applicationId: options.applicationId,
|
|
62
|
+
packageName,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function suggestedAndroidApplicationId(root) {
|
|
66
|
+
const project = basename(root)
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, "");
|
|
69
|
+
return `com.example.${project || "lynxapp"}`;
|
|
70
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { BuildOrchestrator } from "@lynxship/build-orchestrator";
|
|
7
7
|
import { JsonRepository } from "@lynxship/db";
|
|
8
8
|
import { assert, createId, sha256, } from "@lynxship/contracts";
|
|
@@ -10,6 +10,8 @@ import { createSigningKey, signManifest, } from "@lynxship/signing";
|
|
|
10
10
|
import { AppStoreConnectApiProvider, GooglePlayApiProvider, SubmissionService, } from "@lynxship/submit";
|
|
11
11
|
import { DEFAULT_CONFIG, loadConfig, platformValue, } from "./config.js";
|
|
12
12
|
import { hasAndroidHost, isSupportedAndroidPlatform, runRealAndroidBuild, } from "./android-build.js";
|
|
13
|
+
import { initializeAndroidHost, suggestedAndroidApplicationId, } from "./android-host.js";
|
|
14
|
+
import { initializeIosHost, suggestedIosBundleIdentifier } from "./ios-host.js";
|
|
13
15
|
import { hasIosHost, runRealIosBuild } from "./ios-build.js";
|
|
14
16
|
import { configureAndroid, configureAppStoreConnect, configureGooglePlay, configureR2, } from "./configure.js";
|
|
15
17
|
import { fetchOtaPublicKey, publishOtaRelease, rollbackOtaRelease, submitRealArtifact, } from "./remote.js";
|
|
@@ -21,6 +23,7 @@ import { inspectAutolink, requireAutolinkReady } from "./autolink.js";
|
|
|
21
23
|
import { assertCompatibleBinaryBuild, inspectRuntimeFingerprint, } from "./runtime-fingerprint.js";
|
|
22
24
|
import { inspectOtaHost } from "./ota-doctor.js";
|
|
23
25
|
import { otaAssetName, otaAssetPaths } from "./ota-assets.js";
|
|
26
|
+
import { prompt } from "./prompt.js";
|
|
24
27
|
import { commandExists, packageManagerCommand, runProcess, runRspeedy, } from "./process-runner.js";
|
|
25
28
|
const rawArgs = process.argv.slice(2);
|
|
26
29
|
const args = [...rawArgs];
|
|
@@ -52,6 +55,10 @@ function flag(name, fallback = null) {
|
|
|
52
55
|
const index = args.indexOf(name);
|
|
53
56
|
return index >= 0 ? (args[index + 1] ?? "true") : fallback;
|
|
54
57
|
}
|
|
58
|
+
async function assertInteractivePrompt(label, fallback, optionName) {
|
|
59
|
+
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", `Pass ${label.toLowerCase()} with ${optionName} in non-interactive mode.`);
|
|
60
|
+
return prompt(label, fallback);
|
|
61
|
+
}
|
|
55
62
|
function printValue(value, view) {
|
|
56
63
|
if (json) {
|
|
57
64
|
console.log(JSON.stringify(typeof value === "string" ? { result: value } : value));
|
|
@@ -218,7 +225,7 @@ function helpText() {
|
|
|
218
225
|
Commands:
|
|
219
226
|
init Initialize or link a LynxShip project
|
|
220
227
|
doctor Check the local toolchain and project
|
|
221
|
-
dev Run
|
|
228
|
+
dev Run Rspeedy dev with Lynx Explorer QR/HMR
|
|
222
229
|
preview Preview the production Lynx bundle locally
|
|
223
230
|
inspect Inspect Rspeedy/Rspack configuration
|
|
224
231
|
profile Build with Rspack profiling enabled
|
|
@@ -238,6 +245,8 @@ Commands:
|
|
|
238
245
|
rollback Alias for update rollback
|
|
239
246
|
self-host init Generate local self-host credentials
|
|
240
247
|
storage configure Configure Cloudflare R2 and encrypted R2 credentials
|
|
248
|
+
android host init Create a minimal official Lynx Android host
|
|
249
|
+
ios host init Create a minimal official Lynx iOS/Xcode host
|
|
241
250
|
android configure Configure or generate encrypted Android signing credentials
|
|
242
251
|
store configure Configure Google Play or App Store Connect submission
|
|
243
252
|
|
|
@@ -286,6 +295,8 @@ Global options:
|
|
|
286
295
|
--banner Show the Braille LynxShip logo in a TTY
|
|
287
296
|
--project-dir <path> Use a LynxShip project from any working directory
|
|
288
297
|
--project-id <id> Project ID used by init
|
|
298
|
+
--application-id <id> Android package/application ID for host init
|
|
299
|
+
--bundle-identifier <id> iOS bundle identifier for host init
|
|
289
300
|
--library-dir <path> Native library directory for autolink codegen
|
|
290
301
|
--simulator Install an iOS .app on a simulator with simctl
|
|
291
302
|
--help Show this complete command reference
|
|
@@ -365,6 +376,9 @@ async function runRspeedyCommand(subcommand, environment) {
|
|
|
365
376
|
await initializeBuildProject();
|
|
366
377
|
const forwarded = forwardedToolArgs(args);
|
|
367
378
|
ui.info(`Running local Rspeedy ${subcommand}…`);
|
|
379
|
+
if (subcommand === "dev") {
|
|
380
|
+
ui.info("Lynx Explorer mode: no Android or iOS native host is required. Scan the QR code printed by Rspeedy; source changes reload automatically.");
|
|
381
|
+
}
|
|
368
382
|
await runRspeedy(root, subcommand, forwarded, {
|
|
369
383
|
env: environment,
|
|
370
384
|
quiet: json,
|
|
@@ -780,9 +794,88 @@ async function main() {
|
|
|
780
794
|
});
|
|
781
795
|
return;
|
|
782
796
|
}
|
|
797
|
+
if (command === "ios") {
|
|
798
|
+
const subcommand = args.shift() ?? "host";
|
|
799
|
+
assert(subcommand === "host" && (args.shift() ?? "init") === "init", "CLI_IOS_HOST_COMMAND", "Use `lynxship ios host init` to create an iOS host.");
|
|
800
|
+
await initializeBuildProject();
|
|
801
|
+
const suggestedId = suggestedIosBundleIdentifier(root);
|
|
802
|
+
const bundleIdentifier = flag("--bundle-identifier") ??
|
|
803
|
+
(await assertInteractivePrompt("iOS bundle identifier", suggestedId, "--bundle-identifier"));
|
|
804
|
+
const result = await initializeIosHost(root, {
|
|
805
|
+
bundleIdentifier,
|
|
806
|
+
appName: basename(root),
|
|
807
|
+
});
|
|
808
|
+
ui.success(`iOS host created: ${result.directory}`);
|
|
809
|
+
printValue({
|
|
810
|
+
status: "created",
|
|
811
|
+
platform: "ios",
|
|
812
|
+
directory: result.directory,
|
|
813
|
+
bundleIdentifier: result.bundleIdentifier,
|
|
814
|
+
project: result.project,
|
|
815
|
+
scheme: result.scheme,
|
|
816
|
+
configUpdated: result.configUpdated,
|
|
817
|
+
}, {
|
|
818
|
+
title: "iOS host",
|
|
819
|
+
rows: [
|
|
820
|
+
{
|
|
821
|
+
label: "Bundle identifier",
|
|
822
|
+
value: result.bundleIdentifier,
|
|
823
|
+
valueColor: "blue",
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
label: "Xcode project",
|
|
827
|
+
value: result.project,
|
|
828
|
+
valueColor: "green",
|
|
829
|
+
},
|
|
830
|
+
{ label: "Scheme", value: result.scheme, valueColor: "purple" },
|
|
831
|
+
{
|
|
832
|
+
label: "CocoaPods",
|
|
833
|
+
value: "Run pod install on macOS before the first build",
|
|
834
|
+
valueColor: "yellow",
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
done: "iOS host is ready. Run lynxship doctor --platform ios on macOS.",
|
|
838
|
+
});
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
783
841
|
if (command === "android") {
|
|
842
|
+
const subcommand = args.shift() ?? "configure";
|
|
843
|
+
if (subcommand === "host") {
|
|
844
|
+
assert((args.shift() ?? "init") === "init", "CLI_ANDROID_HOST_COMMAND", "Use `lynxship android host init` to create an Android host.");
|
|
845
|
+
await initializeBuildProject();
|
|
846
|
+
const suggestedId = suggestedAndroidApplicationId(root);
|
|
847
|
+
const applicationId = flag("--application-id") ??
|
|
848
|
+
(await assertInteractivePrompt("Android application ID", suggestedId, "--application-id"));
|
|
849
|
+
const result = await initializeAndroidHost(root, {
|
|
850
|
+
applicationId,
|
|
851
|
+
appName: basename(root),
|
|
852
|
+
});
|
|
853
|
+
ui.success(`Android host created: ${result.directory}`);
|
|
854
|
+
printValue({
|
|
855
|
+
status: "created",
|
|
856
|
+
platform: "android",
|
|
857
|
+
directory: result.directory,
|
|
858
|
+
applicationId: result.applicationId,
|
|
859
|
+
}, {
|
|
860
|
+
title: "Android host",
|
|
861
|
+
rows: [
|
|
862
|
+
{
|
|
863
|
+
label: "Application ID",
|
|
864
|
+
value: result.applicationId,
|
|
865
|
+
valueColor: "blue",
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
label: "Gradle wrapper",
|
|
869
|
+
value: join(result.directory, "gradlew"),
|
|
870
|
+
valueColor: "green",
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
done: "Android host is ready. Run lynxship doctor, then lynxship build.",
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
784
877
|
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", "Run `lynxship android configure` in an interactive terminal");
|
|
785
|
-
assert(
|
|
878
|
+
assert(subcommand === "configure", "CLI_ANDROID_COMMAND", "Only android configure is available");
|
|
786
879
|
ui.info("Configuring Android signing. Secret fields will stay invisible…");
|
|
787
880
|
const result = await configureAndroid(root);
|
|
788
881
|
ui.success(result.generated
|
|
@@ -1174,7 +1267,7 @@ async function main() {
|
|
|
1174
1267
|
if (platform === "ios" && !realIos && !args.includes("--local"))
|
|
1175
1268
|
assert(false, "IOS_HOST_REQUIRED", "A macOS Xcode host is required for a real iOS build. No local fake iOS build is created.");
|
|
1176
1269
|
if (platform === "android" && !realAndroid && !args.includes("--local"))
|
|
1177
|
-
assert(false, "ANDROID_HOST_REQUIRED", "
|
|
1270
|
+
assert(false, "ANDROID_HOST_REQUIRED", "This project has no Android Gradle host. For live development, run `lynxship dev` and scan the QR code with Lynx Explorer; for an APK/AAB, integrate the official Lynx Android host under `android/` (including android/gradlew). `--local` is only for contract tests and does not create an APK.");
|
|
1178
1271
|
if (realAndroid) {
|
|
1179
1272
|
await runRealAndroidBuild(job, {
|
|
1180
1273
|
root,
|
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":"AAGA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAShD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,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,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAcxE;
|
|
1
|
+
{"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAShD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,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,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAcxE;AAsDD,wBAAsB,eAAe,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAqMnB"}
|
package/dist/ios-build.js
CHANGED
|
@@ -23,8 +23,15 @@ export function hasIosHost(root, profile) {
|
|
|
23
23
|
}
|
|
24
24
|
function findProject(root, profile) {
|
|
25
25
|
const configured = profile.ios?.workspace ?? profile.ios?.project;
|
|
26
|
-
if (configured)
|
|
27
|
-
|
|
26
|
+
if (configured) {
|
|
27
|
+
const configuredPath = resolve(root, configured);
|
|
28
|
+
if (configuredPath.endsWith(".xcodeproj")) {
|
|
29
|
+
const workspace = configuredPath.replace(/\.xcodeproj$/, ".xcworkspace");
|
|
30
|
+
if (existsSync(workspace))
|
|
31
|
+
return workspace;
|
|
32
|
+
}
|
|
33
|
+
return configuredPath;
|
|
34
|
+
}
|
|
28
35
|
for (const directory of ["ios", "macos"]) {
|
|
29
36
|
try {
|
|
30
37
|
const candidate = readdirSync(join(root, directory)).find((name) => name.endsWith(".xcworkspace") || name.endsWith(".xcodeproj"));
|
|
@@ -37,6 +44,19 @@ function findProject(root, profile) {
|
|
|
37
44
|
}
|
|
38
45
|
throw new Error("No Xcode workspace or project found under ios/ or macos/");
|
|
39
46
|
}
|
|
47
|
+
async function installCocoaPods(root, quiet, onEvent) {
|
|
48
|
+
const iosDirectory = join(root, "ios");
|
|
49
|
+
const podfile = join(iosDirectory, "Podfile");
|
|
50
|
+
if (!existsSync(podfile))
|
|
51
|
+
return;
|
|
52
|
+
assert(commandExists("pod"), "IOS_COCOAPODS_REQUIRED", "CocoaPods was not found. Install CocoaPods on macOS, then rerun the build.");
|
|
53
|
+
onEvent?.("Installing iOS CocoaPods dependencies…");
|
|
54
|
+
await runProcess("pod", ["install"], {
|
|
55
|
+
cwd: iosDirectory,
|
|
56
|
+
quiet,
|
|
57
|
+
onOutput: onEvent,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
40
60
|
async function findIpa(directory) {
|
|
41
61
|
const files = await readdir(directory, { withFileTypes: true });
|
|
42
62
|
const ipa = files.find((file) => file.isFile() && file.name.endsWith(".ipa"));
|
|
@@ -50,6 +70,8 @@ export async function runRealIosBuild(job, options) {
|
|
|
50
70
|
assert(commandExists("xcrun"), "IOS_XCRUN_REQUIRED", "xcrun was not found. Install Xcode command-line tools.");
|
|
51
71
|
const project = findProject(options.root, options.profile);
|
|
52
72
|
assert(existsSync(project), "IOS_PROJECT_REQUIRED", `Configured Xcode project was not found: ${project}`);
|
|
73
|
+
await installCocoaPods(options.root, options.quiet, options.onEvent);
|
|
74
|
+
const resolvedProject = findProject(options.root, options.profile);
|
|
53
75
|
const ios = options.profile.ios ?? {};
|
|
54
76
|
const scheme = ios.scheme;
|
|
55
77
|
assert(scheme, "IOS_SCHEME_REQUIRED", "Configure build.<profile>.ios.scheme in lynxship.json");
|
|
@@ -57,7 +79,7 @@ export async function runRealIosBuild(job, options) {
|
|
|
57
79
|
assert(exportOptions, "IOS_EXPORT_OPTIONS_REQUIRED", "Configure build.<profile>.ios.exportOptionsPlist for a signed IPA export");
|
|
58
80
|
const exportOptionsPath = resolve(options.root, exportOptions);
|
|
59
81
|
assert(existsSync(exportOptionsPath), "IOS_EXPORT_OPTIONS_REQUIRED", `Export options file was not found: ${exportOptionsPath}`);
|
|
60
|
-
const projectFlag =
|
|
82
|
+
const projectFlag = resolvedProject.endsWith(".xcworkspace")
|
|
61
83
|
? "-workspace"
|
|
62
84
|
: "-project";
|
|
63
85
|
const configuration = ios.configuration ?? "Release";
|
|
@@ -92,7 +114,7 @@ export async function runRealIosBuild(job, options) {
|
|
|
92
114
|
step("Preparing Xcode archive…", 10);
|
|
93
115
|
await runProcess("xcodebuild", [
|
|
94
116
|
projectFlag,
|
|
95
|
-
|
|
117
|
+
resolvedProject,
|
|
96
118
|
"-scheme",
|
|
97
119
|
scheme,
|
|
98
120
|
"-configuration",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface IosHostOptions {
|
|
2
|
+
bundleIdentifier: string;
|
|
3
|
+
appName: string;
|
|
4
|
+
}
|
|
5
|
+
export interface IosHostResult {
|
|
6
|
+
directory: string;
|
|
7
|
+
targetName: string;
|
|
8
|
+
bundleIdentifier: string;
|
|
9
|
+
project: string;
|
|
10
|
+
scheme: string;
|
|
11
|
+
exportOptionsPlist: string;
|
|
12
|
+
bundleScript: string;
|
|
13
|
+
configUpdated: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function initializeIosHost(root: string, options: IosHostOptions): Promise<IosHostResult>;
|
|
16
|
+
export declare function suggestedIosBundleIdentifier(root: string): string;
|
|
17
|
+
//# sourceMappingURL=ios-host.d.ts.map
|
|
@@ -0,0 +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;CACjB;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,aAAa,EAAE,OAAO,CAAC;CACxB;AAoED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CA0CxB;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE"}
|
package/dist/ios-host.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { access, cp, readdir, readFile, rename, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { assert } from "@lynxship/contracts";
|
|
5
|
+
const templateRoot = fileURLToPath(new URL("../templates/ios-host/", import.meta.url));
|
|
6
|
+
function validateBundleIdentifier(bundleIdentifier) {
|
|
7
|
+
assert(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*)+$/.test(bundleIdentifier), "IOS_BUNDLE_IDENTIFIER_INVALID", "iOS bundle identifier must contain at least two dot-separated segments, for example com.example.myapp.");
|
|
8
|
+
}
|
|
9
|
+
async function exists(path) {
|
|
10
|
+
try {
|
|
11
|
+
await access(path);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function targetName(value) {
|
|
19
|
+
const words = value.match(/[a-zA-Z0-9]+/g) ?? [];
|
|
20
|
+
const result = words
|
|
21
|
+
.map((word) => `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}`)
|
|
22
|
+
.join("");
|
|
23
|
+
if (result && /^[A-Za-z]/.test(result))
|
|
24
|
+
return result;
|
|
25
|
+
return `LynxShip${result || "App"}`;
|
|
26
|
+
}
|
|
27
|
+
async function textFiles(directory) {
|
|
28
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
29
|
+
const files = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const path = join(directory, entry.name);
|
|
32
|
+
if (entry.isDirectory())
|
|
33
|
+
files.push(...(await textFiles(path)));
|
|
34
|
+
else if ([".h", ".m", ".plist", ".pbxproj", ".swift", ".rb", ".mjs"].some((extension) => entry.name.endsWith(extension)) ||
|
|
35
|
+
entry.name === "Podfile")
|
|
36
|
+
files.push(path);
|
|
37
|
+
}
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
async function updateProjectConfig(root, result) {
|
|
41
|
+
const configPath = join(root, "lynxship.json");
|
|
42
|
+
if (!(await exists(configPath)))
|
|
43
|
+
return false;
|
|
44
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
45
|
+
config.build ??= {};
|
|
46
|
+
config.build.production ??= {};
|
|
47
|
+
config.build.production.ios = {
|
|
48
|
+
...config.build.production.ios,
|
|
49
|
+
project: `ios/${result.targetName}.xcodeproj`,
|
|
50
|
+
scheme: result.scheme,
|
|
51
|
+
configuration: "Release",
|
|
52
|
+
exportOptionsPlist: "ios/ExportOptions.plist",
|
|
53
|
+
bundleScript: "ios/sync-bundle.mjs",
|
|
54
|
+
};
|
|
55
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
export async function initializeIosHost(root, options) {
|
|
59
|
+
validateBundleIdentifier(options.bundleIdentifier);
|
|
60
|
+
const ios = join(root, "ios");
|
|
61
|
+
assert(!(await exists(ios)), "IOS_HOST_EXISTS", `The iOS host already exists at ${ios}. LynxShip will not overwrite it.`);
|
|
62
|
+
const scheme = targetName(options.appName);
|
|
63
|
+
await cp(templateRoot, ios, { recursive: true, force: false });
|
|
64
|
+
await rename(join(ios, "__IOS_TARGET_NAME__"), join(ios, scheme));
|
|
65
|
+
await rename(join(ios, "__IOS_TARGET_NAME__.xcodeproj"), join(ios, `${scheme}.xcodeproj`));
|
|
66
|
+
await rename(join(ios, scheme, "Hello-Lynx-Bridging-Header.h"), join(ios, scheme, `${scheme}-Bridging-Header.h`));
|
|
67
|
+
const files = await textFiles(ios);
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
let content = await readFile(file, "utf8");
|
|
70
|
+
content = content
|
|
71
|
+
.replaceAll("test.Hello-Lynx", options.bundleIdentifier)
|
|
72
|
+
.replaceAll("Hello-Lynx", scheme);
|
|
73
|
+
await writeFile(file, content, "utf8");
|
|
74
|
+
}
|
|
75
|
+
const project = `ios/${scheme}.xcodeproj`;
|
|
76
|
+
const result = {
|
|
77
|
+
directory: ios,
|
|
78
|
+
targetName: scheme,
|
|
79
|
+
bundleIdentifier: options.bundleIdentifier,
|
|
80
|
+
project,
|
|
81
|
+
scheme,
|
|
82
|
+
exportOptionsPlist: "ios/ExportOptions.plist",
|
|
83
|
+
bundleScript: "ios/sync-bundle.mjs",
|
|
84
|
+
};
|
|
85
|
+
const configUpdated = await updateProjectConfig(root, result);
|
|
86
|
+
return { ...result, configUpdated };
|
|
87
|
+
}
|
|
88
|
+
export function suggestedIosBundleIdentifier(root) {
|
|
89
|
+
const project = basename(root)
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/[^a-z0-9]+/g, "");
|
|
92
|
+
return `com.example.${project || "lynxapp"}`;
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynxship/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Build, sign, store, submit and update LynxJS applications from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
],
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
|
-
"README.md"
|
|
28
|
+
"README.md",
|
|
29
|
+
"templates"
|
|
29
30
|
],
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=22.0.0"
|
|
@@ -68,10 +69,10 @@
|
|
|
68
69
|
"ora": "8.2.0",
|
|
69
70
|
"qrcode-terminal": "0.12.0",
|
|
70
71
|
"@lynxship/build-orchestrator": "0.1.0",
|
|
71
|
-
"@lynxship/db": "0.1.0",
|
|
72
72
|
"@lynxship/contracts": "0.1.0",
|
|
73
|
-
"@lynxship/
|
|
74
|
-
"@lynxship/
|
|
73
|
+
"@lynxship/submit": "0.1.0",
|
|
74
|
+
"@lynxship/db": "0.1.0",
|
|
75
|
+
"@lynxship/signing": "0.1.0"
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
77
78
|
"@types/cli-progress": "3.11.6",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id "com.android.application"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
android {
|
|
6
|
+
namespace "__APPLICATION_ID__"
|
|
7
|
+
compileSdk 35
|
|
8
|
+
|
|
9
|
+
defaultConfig {
|
|
10
|
+
applicationId "__APPLICATION_ID__"
|
|
11
|
+
minSdk 24
|
|
12
|
+
targetSdk 35
|
|
13
|
+
versionCode 1
|
|
14
|
+
versionName "0.1.0"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
buildTypes {
|
|
18
|
+
release {
|
|
19
|
+
minifyEnabled false
|
|
20
|
+
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
21
|
+
"proguard-rules.pro"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
compileOptions {
|
|
26
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
27
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dependencies {
|
|
32
|
+
implementation "org.lynxsdk.lynx:lynx:4.0.0"
|
|
33
|
+
implementation "org.lynxsdk.lynx:lynx-jssdk:4.0.0"
|
|
34
|
+
implementation "org.lynxsdk.lynx:lynx-trace:4.0.0"
|
|
35
|
+
implementation "org.lynxsdk.lynx:primjs:4.0.0"
|
|
36
|
+
implementation "org.lynxsdk.lynx:lynx-service-log:4.0.0"
|
|
37
|
+
implementation "org.lynxsdk.lynx:lynx-service-http:4.0.0"
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
4
|
+
|
|
5
|
+
<application
|
|
6
|
+
android:allowBackup="false"
|
|
7
|
+
android:name=".LynxShipApplication"
|
|
8
|
+
android:label="@string/app_name"
|
|
9
|
+
android:supportsRtl="true"
|
|
10
|
+
android:theme="@style/AppTheme">
|
|
11
|
+
<activity
|
|
12
|
+
android:name=".MainActivity"
|
|
13
|
+
android:exported="true">
|
|
14
|
+
<intent-filter>
|
|
15
|
+
<action android:name="android.intent.action.MAIN" />
|
|
16
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
17
|
+
</intent-filter>
|
|
18
|
+
</activity>
|
|
19
|
+
</application>
|
|
20
|
+
</manifest>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package __PACKAGE_NAME__;
|
|
2
|
+
|
|
3
|
+
import android.app.Application;
|
|
4
|
+
|
|
5
|
+
import com.lynx.tasm.LynxEnv;
|
|
6
|
+
|
|
7
|
+
public final class LynxShipApplication extends Application {
|
|
8
|
+
@Override
|
|
9
|
+
public void onCreate() {
|
|
10
|
+
super.onCreate();
|
|
11
|
+
LynxEnv.inst().init(this, null, null, null);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package __PACKAGE_NAME__;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.os.Bundle;
|
|
5
|
+
|
|
6
|
+
import com.lynx.tasm.LynxView;
|
|
7
|
+
import com.lynx.tasm.LynxViewBuilder;
|
|
8
|
+
|
|
9
|
+
public final class MainActivity extends Activity {
|
|
10
|
+
@Override
|
|
11
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
12
|
+
super.onCreate(savedInstanceState);
|
|
13
|
+
LynxView lynxView = new LynxViewBuilder()
|
|
14
|
+
.setTemplateProvider(new ProjectTemplateProvider(this))
|
|
15
|
+
.build(this);
|
|
16
|
+
setContentView(lynxView);
|
|
17
|
+
lynxView.renderTemplateUrl("main.lynx.bundle", "");
|
|
18
|
+
}
|
|
19
|
+
}
|