@lynxship/cli 0.1.3 → 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 +63 -2
- package/dist/android-build.d.ts.map +1 -1
- package/dist/android-build.js +61 -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
|
@@ -97,12 +97,27 @@ Progress percentages are shown only when LynxShip has a real measurement. Long
|
|
|
97
97
|
Rspeedy, Gradle and Xcode operations remain visible in the event journal until
|
|
98
98
|
their completion checkpoint is known; no timer-based percentage is invented.
|
|
99
99
|
|
|
100
|
+
### Project-safe Android signing
|
|
101
|
+
|
|
102
|
+
LynxShip does not require every project to contain a LynxShip-specific signing
|
|
103
|
+
helper. During a real Android build it creates a temporary Gradle init script,
|
|
104
|
+
passes it with Gradle's `--init-script` option, and removes it when the build
|
|
105
|
+
finishes. The script reads only the already configured machine credentials and
|
|
106
|
+
uses the Android Components `finalizeDsl` hook to configure the standard
|
|
107
|
+
`release` build type. The project's Gradle files are left unchanged.
|
|
108
|
+
|
|
109
|
+
This supports normal Android Gradle Plugin application projects using either
|
|
110
|
+
Groovy or Kotlin DSL. Projects with custom flavor tasks, non-standard signing
|
|
111
|
+
plugins, or no Android `release` build type must expose their signing contract
|
|
112
|
+
explicitly; LynxShip fails with a clear diagnostic rather than producing an
|
|
113
|
+
unverified artifact.
|
|
114
|
+
|
|
100
115
|
## Main commands
|
|
101
116
|
|
|
102
117
|
```text
|
|
103
118
|
init Initialize or link a project
|
|
104
119
|
doctor Check the local toolchain and project
|
|
105
|
-
dev Run
|
|
120
|
+
dev Run Rspeedy dev with Lynx Explorer QR/HMR
|
|
106
121
|
preview Preview the production bundle locally
|
|
107
122
|
build create Build, sign and upload an artifact
|
|
108
123
|
build list List build jobs
|
|
@@ -118,7 +133,8 @@ logs Stream native logs
|
|
|
118
133
|
autolink check Check Lynx native-library wiring
|
|
119
134
|
autolink codegen Run native-module codegen
|
|
120
135
|
ota doctor Check native OTA host integration
|
|
121
|
-
storage configure
|
|
136
|
+
storage configure Configure Cloudflare R2
|
|
137
|
+
android host init Create a minimal official Lynx Android host
|
|
122
138
|
android configure Configure Android signing
|
|
123
139
|
store configure Configure store submission credentials
|
|
124
140
|
```
|
|
@@ -155,6 +171,51 @@ command-line tools with `sdkmanager`, accept the required licenses, set
|
|
|
155
171
|
`android/gradlew` is executable. macOS can build both Android and iOS; Windows
|
|
156
172
|
and Linux can build Android only.
|
|
157
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
|
+
|
|
158
219
|
## Package layout
|
|
159
220
|
|
|
160
221
|
The CLI is backed by the public `@lynxship/*` runtime packages in this
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"android-build.d.ts","sourceRoot":"","sources":["../src/android-build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"android-build.d.ts","sourceRoot":"","sources":["../src/android-build.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAWhD,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,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;AA0LD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa7D;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsCpE;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAgKnB"}
|
package/dist/android-build.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { access, copyFile, cp, mkdir, readFile, readdir, } from "node:fs/promises";
|
|
1
|
+
import { access, chmod, copyFile, cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { constants as fsConstants, existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { assert, sha256 } from "@lynxship/contracts";
|
|
6
7
|
import { transitionBuild } from "@lynxship/build-orchestrator";
|
|
@@ -33,6 +34,56 @@ async function signingEnvironment(root) {
|
|
|
33
34
|
assert(missing.length === 0, "BUILD_SIGNING_REQUIRED", `Signed Android builds require configuration. Missing: ${missing.join(", ")}. Run \`lynxship android configure\`.`);
|
|
34
35
|
return { ...process.env, ...values };
|
|
35
36
|
}
|
|
37
|
+
async function createSigningInitScript() {
|
|
38
|
+
const directory = await mkdtemp(join(tmpdir(), "lynxship-gradle-"));
|
|
39
|
+
const file = join(directory, "android-signing.init.gradle");
|
|
40
|
+
const script = `
|
|
41
|
+
def keystorePath = System.getenv("LYNXSHIP_KEYSTORE_PATH")
|
|
42
|
+
def keyAlias = System.getenv("LYNXSHIP_KEY_ALIAS")
|
|
43
|
+
def keystorePassword = System.getenv("LYNXSHIP_KEYSTORE_PASSWORD")
|
|
44
|
+
def keyPassword = System.getenv("LYNXSHIP_KEY_PASSWORD")
|
|
45
|
+
|
|
46
|
+
gradle.allprojects { project ->
|
|
47
|
+
project.plugins.withId("com.android.application") {
|
|
48
|
+
def configureSigning = { android ->
|
|
49
|
+
def signing = android.signingConfigs.findByName("release")
|
|
50
|
+
if (signing == null) {
|
|
51
|
+
signing = android.signingConfigs.create("lynxshipRelease")
|
|
52
|
+
}
|
|
53
|
+
signing.storeFile = project.file(keystorePath)
|
|
54
|
+
signing.storePassword = keystorePassword
|
|
55
|
+
signing.keyAlias = keyAlias
|
|
56
|
+
signing.keyPassword = keyPassword
|
|
57
|
+
|
|
58
|
+
def release = android.buildTypes.findByName("release")
|
|
59
|
+
if (release == null) {
|
|
60
|
+
throw new GradleException(
|
|
61
|
+
"LynxShip requires an Android release build type"
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
release.signingConfig = signing
|
|
65
|
+
project.logger.lifecycle(
|
|
66
|
+
"[LynxShip] Applied machine signing credentials to \${project.path}:release"
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def androidComponents = project.extensions.findByName("androidComponents")
|
|
71
|
+
if (androidComponents != null) {
|
|
72
|
+
androidComponents.finalizeDsl { android ->
|
|
73
|
+
configureSigning(android)
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
project.afterEvaluate {
|
|
77
|
+
configureSigning(project.extensions.findByName("android"))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
await writeFile(file, script, "utf8");
|
|
84
|
+
await chmod(file, 0o600).catch(() => undefined);
|
|
85
|
+
return { directory, file };
|
|
86
|
+
}
|
|
36
87
|
async function verifySignedArtifact(root, artifactPath, options) {
|
|
37
88
|
if (artifactPath.endsWith(".apk")) {
|
|
38
89
|
const apksigner = androidTool("apksigner");
|
|
@@ -161,6 +212,7 @@ export async function runRealAndroidBuild(job, options) {
|
|
|
161
212
|
quiet: options.quiet ?? false,
|
|
162
213
|
onOutput: options.onEvent,
|
|
163
214
|
};
|
|
215
|
+
let signingInitDirectory;
|
|
164
216
|
try {
|
|
165
217
|
transitionBuild(job, "uploading_source", "local Android source prepared");
|
|
166
218
|
job.logs.push({
|
|
@@ -189,7 +241,10 @@ export async function runRealAndroidBuild(job, options) {
|
|
|
189
241
|
step("Android host synchronized", 40);
|
|
190
242
|
transitionBuild(job, "building", `Gradle ${artifact.task}`);
|
|
191
243
|
step(`Running real Gradle task ${artifact.task}…`);
|
|
192
|
-
|
|
244
|
+
step("Applying temporary LynxShip Android signing adapter…");
|
|
245
|
+
const signingInitScript = await createSigningInitScript();
|
|
246
|
+
signingInitDirectory = signingInitScript.directory;
|
|
247
|
+
const gradleArgs = ["--init-script", signingInitScript.file, artifact.task];
|
|
193
248
|
if (process.env.CI)
|
|
194
249
|
gradleArgs.push("--stacktrace");
|
|
195
250
|
await runProcess(wrapper, gradleArgs, {
|
|
@@ -258,4 +313,8 @@ export async function runRealAndroidBuild(job, options) {
|
|
|
258
313
|
});
|
|
259
314
|
throw error;
|
|
260
315
|
}
|
|
316
|
+
finally {
|
|
317
|
+
if (signingInitDirectory)
|
|
318
|
+
await rm(signingInitDirectory, { recursive: true, force: true });
|
|
319
|
+
}
|
|
261
320
|
}
|
|
@@ -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
|
+
}
|