@absolutejs/absolute 0.20.0-beta.1 → 0.20.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -774,6 +774,14 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
774
774
  throw new TypeError("mobile.deepLinks.apple.appIdPrefix must contain ten letters or digits.");
775
775
  }
776
776
  return normalized;
777
+ }, normalizeIosVersion = (value) => {
778
+ if (value === undefined)
779
+ return;
780
+ const normalized = requireText(value, "mobile.ios.version");
781
+ if (!/^\d+(?:\.\d+){0,2}$/u.test(normalized)) {
782
+ throw new TypeError("mobile.ios.version must contain one to three dot-separated integer components, for example 1.4.0.");
783
+ }
784
+ return normalized;
777
785
  }, normalizeCertificateFingerprints = (values) => [
778
786
  ...new Set((values ?? []).map((value) => requireText(value, "mobile.deepLinks.android.sha256CertificateFingerprints").replaceAll(":", "").toUpperCase()).map((value) => {
779
787
  if (!CERTIFICATE_FINGERPRINT_PATTERN.test(value)) {
@@ -801,6 +809,7 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
801
809
  deepLinkScheme,
802
810
  engine: "capacitor",
803
811
  entry: normalizeEntry(config.entry),
812
+ iosVersion: normalizeIosVersion(config.ios?.version),
804
813
  nativeProjectDirectory: resolveProjectPath(projectRoot, config.nativeProject?.directory ?? "mobile", "mobile.nativeProject.directory"),
805
814
  platforms: normalizePlatforms(config.platforms),
806
815
  productionOrigin
@@ -1643,12 +1652,13 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1643
1652
  throw new Error("Capacitor Android settings did not declare any native dependencies.");
1644
1653
  }
1645
1654
  return { dependencies, rewrittenSettings };
1646
- }, encodedWindowsGradleCommand = (windowsSource, windowsDirectory, windowsAndroidRoot, dependencies, rewrittenSettings, task) => {
1655
+ }, encodedWindowsGradleCommand = (windowsSource, windowsDirectory, windowsAndroidRoot, dependencies, rewrittenSettings, task, gradleArguments) => {
1647
1656
  const sourceDirectory = Buffer.from(windowsSource, "utf8").toString("base64");
1648
1657
  const buildDirectory = Buffer.from(windowsDirectory, "utf8").toString("base64");
1649
1658
  const androidRoot = Buffer.from(windowsAndroidRoot, "utf8").toString("base64");
1650
1659
  const dependencyData = Buffer.from(JSON.stringify(dependencies), "utf8").toString("base64");
1651
1660
  const settingsData = Buffer.from(rewrittenSettings, "utf8").toString("base64");
1661
+ const argumentsData = Buffer.from(JSON.stringify(gradleArguments), "utf8").toString("base64");
1652
1662
  const source = [
1653
1663
  "$ErrorActionPreference = 'Stop'",
1654
1664
  `$source = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${sourceDirectory}'))`,
@@ -1656,6 +1666,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1656
1666
  `$androidHome = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${androidRoot}'))`,
1657
1667
  `$dependencies = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${dependencyData}')) | ConvertFrom-Json`,
1658
1668
  `$settings = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${settingsData}'))`,
1669
+ `$gradleArguments = @([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${argumentsData}')) | ConvertFrom-Json)`,
1659
1670
  "$env:ANDROID_HOME = $androidHome",
1660
1671
  "$env:ANDROID_SDK_ROOT = $androidHome",
1661
1672
  "New-Item -ItemType Directory -Force -Path $directory | Out-Null",
@@ -1665,7 +1676,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1665
1676
  "foreach ($dependency in @($dependencies)) { $target = Join-Path $directory ('.absolutejs-dependencies\\' + $dependency.name); New-Item -ItemType Directory -Force -Path $target | Out-Null; & robocopy.exe $dependency.windowsSource $target /MIR /XD .gradle build /NFL /NDL /NJH /NJS /NP; if ($LASTEXITCODE -ge 8) { exit $LASTEXITCODE } }",
1666
1677
  "[IO.File]::WriteAllText((Join-Path $directory 'capacitor.settings.gradle'), $settings)",
1667
1678
  "$wrapper = Join-Path $directory 'gradlew.bat'",
1668
- `& $wrapper --no-daemon --console=plain -p $directory ${task}`,
1679
+ `& $wrapper --no-daemon --console=plain -p $directory @gradleArguments ${task}`,
1669
1680
  "if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }",
1670
1681
  "exit 0"
1671
1682
  ].join("; ");
@@ -1690,6 +1701,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1690
1701
  const capture = options.capture ?? captureCommand2;
1691
1702
  const run = options.run ?? runCommand;
1692
1703
  const env = options.env ?? process.env;
1704
+ const gradleArguments = options.gradleArguments ?? [];
1693
1705
  if (project.host === "wsl") {
1694
1706
  const windowsSource = windowsPathFromWsl(project.nativeDirectory, capture);
1695
1707
  const buildId = Bun.hash(project.projectRoot).toString(HASH_RADIX);
@@ -1701,7 +1713,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1701
1713
  "powershell.exe",
1702
1714
  "-NoProfile",
1703
1715
  "-EncodedCommand",
1704
- encodedWindowsGradleCommand(windowsSource, windowsDirectory, windowsAndroidRoot, dependencies, rewrittenSettings, task)
1716
+ encodedWindowsGradleCommand(windowsSource, windowsDirectory, windowsAndroidRoot, dependencies, rewrittenSettings, task, gradleArguments)
1705
1717
  ], "Android Gradle build", run, { env, signal: options.signal });
1706
1718
  const artifactPath2 = await resolveGradleArtifactPath(managedBuildDirectory, task);
1707
1719
  const unsigned = artifactPath2.endsWith("-unsigned.apk");
@@ -1711,7 +1723,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1711
1723
  };
1712
1724
  }
1713
1725
  const wrapper = project.host === "windows" ? "gradlew.bat" : "./gradlew";
1714
- await requireSuccess([wrapper, "--no-daemon", "--console=plain", task], "Android Gradle build", run, { cwd: project.nativeDirectory, env, signal: options.signal });
1726
+ await requireSuccess([wrapper, "--no-daemon", "--console=plain", ...gradleArguments, task], "Android Gradle build", run, { cwd: project.nativeDirectory, env, signal: options.signal });
1715
1727
  const artifactPath = await resolveGradleArtifactPath(project.nativeDirectory, task);
1716
1728
  return { artifactPath, installPath: artifactPath };
1717
1729
  }, buildAndroidDebugApp = async (project, capture, run, env, signal) => (await buildAbsoluteAndroidGradleArtifact({
@@ -14523,10 +14535,40 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists3 = async (path) => {
14523
14535
  ...check2,
14524
14536
  path: check2.path ? relative19(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
14525
14537
  }));
14538
+ }, inspectIosRelease = async (config, projectRoot) => {
14539
+ const iosAppRoot = join41(config.nativeProjectDirectory, "ios", "App", "App");
14540
+ const nativeConfigPath = join41(iosAppRoot, "capacitor.config.json");
14541
+ const infoPath = join41(iosAppRoot, "Info.plist");
14542
+ const publicRoot = join41(iosAppRoot, "public");
14543
+ const checks = [];
14544
+ if (!config.iosVersion) {
14545
+ checks.push(fail5("ios.marketing-version", "iOS has no explicit App Store marketing version.", projectRoot, "Add mobile.ios.version to absolutejs.config.ts, for example 1.0.0."));
14546
+ } else {
14547
+ checks.push(pass("ios.marketing-version", `The iOS marketing version is ${config.iosVersion}.`));
14548
+ }
14549
+ if (!await pathExists3(nativeConfigPath)) {
14550
+ checks.push(fail5("ios.capacitor-config", "The generated iOS Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync ios` before release validation."));
14551
+ } else if (isUnsafeCapacitorConfig(await readFile10(nativeConfigPath, "utf8"))) {
14552
+ checks.push(fail5("ios.capacitor-config", "iOS Capacitor config contains a development server URL, cleartext transport, navigation allowlist, or invalid JSON.", nativeConfigPath, "Run `absolute mobile sync ios`; do not ship development transport overrides."));
14553
+ } else {
14554
+ checks.push(pass("ios.capacitor-config", "iOS Capacitor config contains no development transport overrides.", nativeConfigPath));
14555
+ }
14556
+ if (!await pathExists3(infoPath)) {
14557
+ checks.push(fail5("ios.transport-security", "The iOS Info.plist is missing.", infoPath, "Run `absolute mobile sync ios` before release validation."));
14558
+ } else {
14559
+ const info2 = await readFile10(infoPath, "utf8");
14560
+ checks.push(/<key>NSAllowsArbitraryLoads<\/key>\s*<true\s*\/>/u.test(info2) ? fail5("ios.transport-security", "iOS App Transport Security permits arbitrary network loads.", infoPath, "Remove NSAllowsArbitraryLoads from the release Info.plist.") : pass("ios.transport-security", "iOS App Transport Security does not permit arbitrary loads.", infoPath));
14561
+ }
14562
+ const hmrAsset = await findHmrAsset(publicRoot);
14563
+ checks.push(hmrAsset ? fail5("ios.hmr-assets", "A packaged iOS asset contains the development HMR client.", hmrAsset, "Rebuild the production mobile bundle and run Capacitor sync again.") : pass("ios.hmr-assets", "Packaged iOS assets contain no development HMR markers.", publicRoot));
14564
+ return checks.map((check2) => ({
14565
+ ...check2,
14566
+ path: check2.path ? relative19(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
14567
+ }));
14526
14568
  }, inspectAbsoluteMobileRelease = async (config, projectRoot) => {
14527
14569
  const checks = config.platforms.includes("android") ? await inspectAndroidRelease(config, projectRoot) : [];
14528
14570
  if (config.platforms.includes("ios")) {
14529
- checks.push(fail5("ios.release-validation", "iOS release validation is not available in this Android phase.", config.nativeProjectDirectory, "Do not treat this result as release-ready until the iOS release checks ship."));
14571
+ checks.push(...await inspectIosRelease(config, projectRoot));
14530
14572
  }
14531
14573
  return {
14532
14574
  checks,
@@ -14651,12 +14693,32 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord11 = (value) => typeof value ==
14651
14693
  }
14652
14694
  return { ...expected, artifact };
14653
14695
  }, buildAbsoluteAndroidRelease = async (options) => {
14696
+ if (options.versionCode !== undefined && options.prepareVersionCode) {
14697
+ throw new TypeError("Android release versionCode and prepareVersionCode cannot be combined.");
14698
+ }
14699
+ if (options.versionCode !== undefined && (!Number.isSafeInteger(options.versionCode) || options.versionCode < 1 || options.versionCode > 2100000000)) {
14700
+ throw new TypeError("Android versionCode must be an integer from 1 through 2100000000.");
14701
+ }
14654
14702
  const projectRoot = resolve35(options.projectRoot);
14655
14703
  const host = options.host ?? detectAbsoluteMobileHost();
14656
14704
  const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host);
14657
14705
  const nativeDirectory = join42(options.config.nativeProjectDirectory, "android");
14706
+ const manifest = requireManifest(JSON.parse(await readFile11(join42(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
14707
+ if (manifest.appId !== options.config.appId) {
14708
+ throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
14709
+ }
14710
+ let { versionCode } = options;
14711
+ if (options.prepareVersionCode) {
14712
+ const nativeFingerprint = await fingerprintAbsoluteAndroidNativeProject({ nativeDirectory });
14713
+ const buildIdentity = createHash9("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
14714
+ versionCode = await options.prepareVersionCode(buildIdentity);
14715
+ }
14716
+ if (versionCode !== undefined && (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000)) {
14717
+ throw new TypeError("Android versionCode must be an integer from 1 through 2100000000.");
14718
+ }
14658
14719
  const { artifactPath } = await buildAbsoluteAndroidGradleArtifact({
14659
14720
  capture: options.capture,
14721
+ gradleArguments: versionCode === undefined ? [] : [`-Pandroid.injected.version.code=${versionCode}`],
14660
14722
  project: {
14661
14723
  androidRoot,
14662
14724
  config: options.config,
@@ -14678,10 +14740,6 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord11 = (value) => typeof value ==
14678
14740
  if (!signed2 && !options.allowUnsigned) {
14679
14741
  throw new TypeError("Android Gradle produced an unsigned App Bundle. Configure the release signingConfig in the source-owned Android project (prefer external Gradle properties), or pass --unsigned only for a non-publishable build.");
14680
14742
  }
14681
- const manifest = requireManifest(JSON.parse(await readFile11(join42(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
14682
- if (manifest.appId !== options.config.appId) {
14683
- throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
14684
- }
14685
14743
  const [bytes, sha2562] = await Promise.all([
14686
14744
  stat(artifactPath).then(({ size }) => size),
14687
14745
  sha256File(artifactPath)
@@ -14698,7 +14756,8 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord11 = (value) => typeof value ==
14698
14756
  runtime: manifest.runtime,
14699
14757
  sha256: sha2562,
14700
14758
  signed: signed2 === true,
14701
- type: "aab"
14759
+ type: "aab",
14760
+ ...versionCode === undefined ? {} : { versionCode }
14702
14761
  };
14703
14762
  return installRelease(artifactPath, metadata, safeOutputDirectory(projectRoot, options.outputDirectory));
14704
14763
  };
@@ -14707,13 +14766,377 @@ var init_androidRelease = __esm(() => {
14707
14766
  init_androidEmulatorController();
14708
14767
  });
14709
14768
 
14769
+ // src/mobile/iosRelease.ts
14770
+ import { createHash as createHash10 } from "crypto";
14771
+ import {
14772
+ access as access8,
14773
+ copyFile as copyFile4,
14774
+ mkdir as mkdir9,
14775
+ mkdtemp as mkdtemp5,
14776
+ readdir as readdir4,
14777
+ readFile as readFile12,
14778
+ rename as rename8,
14779
+ rm as rm7,
14780
+ stat as stat2,
14781
+ writeFile as writeFile9
14782
+ } from "fs/promises";
14783
+ import { dirname as dirname25, isAbsolute as isAbsolute5, join as join43, relative as relative21, resolve as resolve36, sep as sep4 } from "path";
14784
+ var isRecord12 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
14785
+ if (!isRecord12(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
14786
+ throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
14787
+ }
14788
+ return {
14789
+ appBuild: value.appBuild,
14790
+ appId: value.appId,
14791
+ runtime: value.runtime
14792
+ };
14793
+ }, pathExists5 = async (path) => {
14794
+ try {
14795
+ await access8(path);
14796
+ return true;
14797
+ } catch {
14798
+ return false;
14799
+ }
14800
+ }, defaultRun2 = async (command, options = {}) => {
14801
+ const process2 = Bun.spawn(command, {
14802
+ cwd: options.cwd,
14803
+ env: options.env,
14804
+ stderr: "inherit",
14805
+ stdin: "inherit",
14806
+ stdout: "inherit"
14807
+ });
14808
+ return process2.exited;
14809
+ }, defaultCapture3 = (command, options = {}) => {
14810
+ try {
14811
+ const result = Bun.spawnSync(command, {
14812
+ cwd: options.cwd,
14813
+ env: options.env,
14814
+ stderr: "pipe",
14815
+ stdin: "ignore",
14816
+ stdout: "pipe"
14817
+ });
14818
+ return {
14819
+ exitCode: result.exitCode,
14820
+ stderr: result.stderr.toString(),
14821
+ stdout: result.stdout.toString()
14822
+ };
14823
+ } catch (error) {
14824
+ return {
14825
+ exitCode: 1,
14826
+ stderr: error instanceof Error ? error.message : String(error),
14827
+ stdout: ""
14828
+ };
14829
+ }
14830
+ }, ignoredFingerprintDirectories, fingerprintFiles = async (root, current = root) => {
14831
+ const entries = await readdir4(current, { withFileTypes: true });
14832
+ const nested = await Promise.all(entries.sort((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
14833
+ const path = join43(current, entry.name);
14834
+ const projectRelative = relative21(root, path).replaceAll("\\", "/");
14835
+ const ignored = entry.isDirectory() && (ignoredFingerprintDirectories.has(entry.name) || projectRelative === "App/App/public");
14836
+ if (ignored)
14837
+ return [];
14838
+ if (entry.isDirectory())
14839
+ return fingerprintFiles(root, path);
14840
+ return entry.isFile() ? [path] : [];
14841
+ }));
14842
+ return nested.flat();
14843
+ }, fingerprintAbsoluteIosNativeProject = async (nativeDirectory) => {
14844
+ const hasher = createHash10("sha256");
14845
+ const files = await fingerprintFiles(nativeDirectory);
14846
+ const contents = await Promise.all(files.map((file) => readFile12(file)));
14847
+ files.forEach((file, index) => {
14848
+ hasher.update(relative21(nativeDirectory, file).replaceAll("\\", "/"));
14849
+ hasher.update("\x00");
14850
+ hasher.update(contents[index] ?? new Uint8Array);
14851
+ hasher.update("\x00");
14852
+ });
14853
+ return hasher.digest("hex");
14854
+ }, safeOutputDirectory2 = (projectRoot, requested) => {
14855
+ const root = resolve36(projectRoot);
14856
+ const output = resolve36(root, requested ?? ".absolutejs/mobile/releases/ios");
14857
+ const projectRelative = relative21(root, output);
14858
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep4}`) || isAbsolute5(projectRelative)) {
14859
+ throw new TypeError("mobile build --outdir must remain inside the project.");
14860
+ }
14861
+ return output;
14862
+ }, sha256File2 = async (path) => createHash10("sha256").update(await readFile12(path)).digest("hex"), findByExtension = async (root, extension) => {
14863
+ if (!await pathExists5(root))
14864
+ return;
14865
+ const entries = await readdir4(root, { withFileTypes: true });
14866
+ const matches = await Promise.all(entries.map(async (entry) => {
14867
+ const path = join43(root, entry.name);
14868
+ if (entry.isDirectory() && entry.name.endsWith(extension))
14869
+ return path;
14870
+ if (entry.isFile() && entry.name.endsWith(extension))
14871
+ return path;
14872
+ return entry.isDirectory() ? findByExtension(path, extension) : undefined;
14873
+ }));
14874
+ return matches.find((match) => match !== undefined);
14875
+ }, exportOptions = () => `<?xml version="1.0" encoding="UTF-8"?>
14876
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
14877
+ <plist version="1.0"><dict>
14878
+ <key>destination</key><string>export</string>
14879
+ <key>manageAppVersionAndBuildNumber</key><false/>
14880
+ <key>method</key><string>app-store-connect</string>
14881
+ <key>signingStyle</key><string>automatic</string>
14882
+ <key>stripSwiftSymbols</key><true/>
14883
+ <key>uploadSymbols</key><true/>
14884
+ </dict></plist>
14885
+ `, requireBuildNumber = (value) => {
14886
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
14887
+ throw new TypeError("iOS build number must be a positive integer.");
14888
+ return value;
14889
+ }, installRelease2 = async (artifactPath, metadata, outputRoot) => {
14890
+ const releaseRoot = join43(outputRoot, metadata.releaseId);
14891
+ const destination = join43(releaseRoot, "App.ipa");
14892
+ if (await pathExists5(releaseRoot)) {
14893
+ const value = JSON.parse(await readFile12(join43(releaseRoot, "release.json"), "utf8"));
14894
+ if (!isRecord12(value) || value.artifact !== "App.ipa" || Object.entries(metadata).some(([key, expected]) => Reflect.get(value, key) !== expected)) {
14895
+ throw new TypeError(`Immutable iOS release ${metadata.releaseId} does not match its content.`);
14896
+ }
14897
+ const [bytes, sha2562] = await Promise.all([
14898
+ stat2(destination).then(({ size }) => size),
14899
+ sha256File2(destination)
14900
+ ]);
14901
+ if (bytes !== metadata.bytes || sha2562 !== metadata.sha256)
14902
+ throw new TypeError(`Immutable iOS release ${metadata.releaseId} artifact is missing or modified.`);
14903
+ return {
14904
+ artifactPath: destination,
14905
+ metadata: {
14906
+ ...metadata,
14907
+ artifact: "App.ipa"
14908
+ },
14909
+ releaseRoot
14910
+ };
14911
+ }
14912
+ await mkdir9(dirname25(releaseRoot), { recursive: true });
14913
+ const staging = await mkdtemp5(join43(dirname25(releaseRoot), ".ios-stage-"));
14914
+ try {
14915
+ await copyFile4(artifactPath, join43(staging, "App.ipa"));
14916
+ const complete = {
14917
+ ...metadata,
14918
+ artifact: "App.ipa"
14919
+ };
14920
+ await writeFile9(join43(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
14921
+ `, { flag: "wx" });
14922
+ await rename8(staging, releaseRoot);
14923
+ return { artifactPath: destination, metadata: complete, releaseRoot };
14924
+ } finally {
14925
+ await rm7(staging, { force: true, recursive: true }).catch(() => {
14926
+ return;
14927
+ });
14928
+ }
14929
+ }, buildAbsoluteIosRelease = async (options) => {
14930
+ if (options.buildNumber !== undefined && options.prepareBuildNumber)
14931
+ throw new TypeError("iOS release buildNumber and prepareBuildNumber cannot be combined.");
14932
+ if ((options.host ?? detectAbsoluteMobileHost()) !== "macos")
14933
+ throw new TypeError("iOS release builds require macOS and Xcode.");
14934
+ const marketingVersion = options.config.iosVersion;
14935
+ if (!marketingVersion)
14936
+ throw new TypeError("iOS release builds require mobile.ios.version in absolutejs.config.ts.");
14937
+ const manifest = requireManifest2(JSON.parse(await readFile12(join43(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
14938
+ if (manifest.appId !== options.config.appId)
14939
+ throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
14940
+ const nativeDirectory = join43(options.config.nativeProjectDirectory, "ios");
14941
+ let buildNumber = requireBuildNumber(options.buildNumber);
14942
+ if (options.prepareBuildNumber) {
14943
+ const nativeFingerprint = await fingerprintAbsoluteIosNativeProject(nativeDirectory);
14944
+ const buildIdentity = createHash10("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}\x00${marketingVersion}`).digest("hex");
14945
+ buildNumber = requireBuildNumber(await options.prepareBuildNumber(buildIdentity));
14946
+ }
14947
+ const stagingParent = resolve36(options.projectRoot, ".absolutejs/mobile");
14948
+ await mkdir9(stagingParent, { recursive: true });
14949
+ const staging = await mkdtemp5(join43(stagingParent, ".ios-build-"));
14950
+ const archivePath = join43(staging, "App.xcarchive");
14951
+ const exportPath = join43(staging, "export");
14952
+ const exportPlist = join43(staging, "ExportOptions.plist");
14953
+ await mkdir9(exportPath, { recursive: true });
14954
+ await writeFile9(exportPlist, exportOptions());
14955
+ const run = options.run ?? defaultRun2;
14956
+ try {
14957
+ const versionArguments = [
14958
+ `MARKETING_VERSION=${marketingVersion}`,
14959
+ ...buildNumber === undefined ? [] : [`CURRENT_PROJECT_VERSION=${buildNumber}`]
14960
+ ];
14961
+ const archiveExit = await run([
14962
+ "xcodebuild",
14963
+ "-workspace",
14964
+ join43(nativeDirectory, "App", "App.xcworkspace"),
14965
+ "-scheme",
14966
+ "App",
14967
+ "-configuration",
14968
+ "Release",
14969
+ "-destination",
14970
+ "generic/platform=iOS",
14971
+ "-archivePath",
14972
+ archivePath,
14973
+ ...versionArguments,
14974
+ "archive"
14975
+ ], { cwd: nativeDirectory });
14976
+ if (archiveExit !== 0)
14977
+ throw new TypeError("Xcode failed to archive the iOS app.");
14978
+ const archivedApp = await findByExtension(join43(archivePath, "Products", "Applications"), ".app");
14979
+ const capture = options.capture ?? defaultCapture3;
14980
+ const signed2 = archivedApp ? capture([
14981
+ "codesign",
14982
+ "--verify",
14983
+ "--deep",
14984
+ "--strict",
14985
+ archivedApp
14986
+ ]).exitCode === 0 : false;
14987
+ if (!signed2 && !options.allowUnsigned)
14988
+ throw new TypeError("Xcode produced an unsigned iOS archive. Configure signing in the source-owned Xcode project, or pass --unsigned only for a non-publishable build.");
14989
+ const exportExit = await run([
14990
+ "xcodebuild",
14991
+ "-exportArchive",
14992
+ "-archivePath",
14993
+ archivePath,
14994
+ "-exportPath",
14995
+ exportPath,
14996
+ "-exportOptionsPlist",
14997
+ exportPlist
14998
+ ], { cwd: nativeDirectory });
14999
+ if (exportExit !== 0)
15000
+ throw new TypeError("Xcode failed to export the App Store IPA.");
15001
+ const artifactPath = await findByExtension(exportPath, ".ipa");
15002
+ if (!artifactPath)
15003
+ throw new TypeError("Xcode did not produce an exported IPA.");
15004
+ const [bytes, sha2562] = await Promise.all([
15005
+ stat2(artifactPath).then(({ size }) => size),
15006
+ sha256File2(artifactPath)
15007
+ ]);
15008
+ const releaseId = `amobile_ios_${sha2562}`;
15009
+ return await installRelease2(artifactPath, {
15010
+ appBuild: manifest.appBuild,
15011
+ appId: manifest.appId,
15012
+ ...buildNumber === undefined ? {} : { buildNumber },
15013
+ bytes,
15014
+ engine: "capacitor",
15015
+ format: 1,
15016
+ marketingVersion,
15017
+ platform: "ios",
15018
+ releaseId,
15019
+ runtime: manifest.runtime,
15020
+ sha256: sha2562,
15021
+ signed: signed2,
15022
+ type: "ipa"
15023
+ }, safeOutputDirectory2(options.projectRoot, options.outputDirectory));
15024
+ } finally {
15025
+ await rm7(staging, { force: true, recursive: true }).catch(() => {
15026
+ return;
15027
+ });
15028
+ }
15029
+ };
15030
+ var init_iosRelease = __esm(() => {
15031
+ init_emulatorDoctor();
15032
+ ignoredFingerprintDirectories = new Set([
15033
+ "Pods",
15034
+ "DerivedData",
15035
+ "build",
15036
+ "xcuserdata"
15037
+ ]);
15038
+ });
15039
+
15040
+ // src/mobile/releasePublisher.ts
15041
+ import { access as access9 } from "fs/promises";
15042
+ import { isAbsolute as isAbsolute6, relative as relative22, resolve as resolve37, sep as sep5 } from "path";
15043
+ import { pathToFileURL as pathToFileURL2 } from "url";
15044
+ var prepareAbsoluteIosRelease = async (publisher, options) => {
15045
+ if (typeof publisher.prepareIosRelease !== "function") {
15046
+ throw new TypeError("App Store Connect publishing requires a registry module created with @absolutejs/deploy/app-store-connect.");
15047
+ }
15048
+ const { buildNumber } = await publisher.prepareIosRelease(options);
15049
+ if (!Number.isSafeInteger(buildNumber) || buildNumber < 1) {
15050
+ throw new TypeError("App Store Connect publisher returned an invalid iOS build number.");
15051
+ }
15052
+ return buildNumber;
15053
+ }, prepareAbsoluteAndroidRelease = async (publisher, options) => {
15054
+ if (typeof publisher.prepareAndroidRelease !== "function") {
15055
+ throw new TypeError("Google Play publishing requires a registry module created with @absolutejs/deploy/google-play.");
15056
+ }
15057
+ const prepared = await publisher.prepareAndroidRelease(options);
15058
+ const { versionCode } = prepared;
15059
+ if (typeof versionCode !== "number" || !Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000) {
15060
+ throw new TypeError("Google Play publisher returned an invalid Android versionCode.");
15061
+ }
15062
+ return versionCode;
15063
+ }, isRecord13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isPublisher = (value) => isRecord13(value) && typeof value.publish === "function", publisherModulePath = (projectRoot, requested) => {
15064
+ const root = resolve37(projectRoot);
15065
+ const path = resolve37(root, requested);
15066
+ const projectRelative = relative22(root, path);
15067
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep5}`) || isAbsolute6(projectRelative)) {
15068
+ throw new TypeError("mobile publish --registry must remain inside the project.");
15069
+ }
15070
+ return path;
15071
+ }, loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath) => {
15072
+ const modulePath = publisherModulePath(projectRoot, requestedModulePath);
15073
+ await access9(modulePath).catch(() => {
15074
+ throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
15075
+ });
15076
+ const loaded = await import(pathToFileURL2(modulePath).href);
15077
+ const publisher = isRecord13(loaded) ? loaded.default ?? loaded.registry : undefined;
15078
+ if (!isPublisher(publisher)) {
15079
+ throw new TypeError("Native release registry module must default-export a registry with publish(options).");
15080
+ }
15081
+ return publisher;
15082
+ }, publishAbsoluteAndroidRelease = async (options) => {
15083
+ const publisher = await loadAbsoluteNativeReleasePublisher(options.projectRoot, options.modulePath);
15084
+ const publication = await publisher.publish({
15085
+ allowUnsigned: options.allowUnsigned,
15086
+ channel: options.channel,
15087
+ googlePlay: options.googlePlay,
15088
+ releaseRoot: options.release.releaseRoot,
15089
+ signal: options.signal
15090
+ });
15091
+ const expected = options.release.metadata;
15092
+ const actual = publication.record?.metadata;
15093
+ if (!actual || actual.appId !== expected.appId || actual.platform !== "android" || actual.releaseId !== expected.releaseId || actual.sha256 !== expected.sha256 || actual.signed !== expected.signed || actual.versionCode !== expected.versionCode || typeof publication.reused !== "boolean") {
15094
+ throw new TypeError("Native release registry returned a different Android release identity.");
15095
+ }
15096
+ if (options.channel !== undefined && (publication.channel?.channel !== options.channel || publication.channel.releaseId !== expected.releaseId)) {
15097
+ throw new TypeError("Native release registry did not promote the requested channel.");
15098
+ }
15099
+ const { googlePlay } = publication;
15100
+ if (options.googlePlay) {
15101
+ if (!expected.versionCode || !googlePlay || googlePlay.receipt.provider !== "google-play" || googlePlay.receipt.packageName !== expected.appId || googlePlay.receipt.releaseId !== expected.releaseId || googlePlay.receipt.sha256 !== expected.sha256 || googlePlay.receipt.stage !== "committed" || googlePlay.receipt.intent.track !== options.googlePlay.track || typeof googlePlay.receipt.versionCode !== "string" || !/^\d+$/.test(googlePlay.receipt.versionCode) || Number(googlePlay.receipt.versionCode) !== expected.versionCode || typeof googlePlay.reused !== "boolean") {
15102
+ throw new TypeError("Native release publisher did not commit the requested Google Play release.");
15103
+ }
15104
+ }
15105
+ return publication;
15106
+ }, publishAbsoluteIosRelease = async (options) => {
15107
+ const publisher = await loadAbsoluteNativeReleasePublisher(options.projectRoot, options.modulePath);
15108
+ const publication = await publisher.publish({
15109
+ allowUnsigned: options.allowUnsigned,
15110
+ appStoreConnect: options.appStoreConnect,
15111
+ channel: options.channel,
15112
+ releaseRoot: options.release.releaseRoot,
15113
+ signal: options.signal
15114
+ });
15115
+ const expected = options.release.metadata;
15116
+ const actual = publication.record?.metadata;
15117
+ if (!actual || actual.appId !== expected.appId || actual.platform !== "ios" || actual.releaseId !== expected.releaseId || actual.sha256 !== expected.sha256 || actual.signed !== expected.signed || actual.buildNumber !== expected.buildNumber || actual.marketingVersion !== expected.marketingVersion || typeof publication.reused !== "boolean") {
15118
+ throw new TypeError("Native release registry returned a different iOS release identity.");
15119
+ }
15120
+ if (options.channel !== undefined && (publication.channel?.channel !== options.channel || publication.channel.releaseId !== expected.releaseId)) {
15121
+ throw new TypeError("Native release registry did not promote the requested channel.");
15122
+ }
15123
+ if (options.appStoreConnect) {
15124
+ const distributed = publication.appStoreConnect;
15125
+ if (!expected.buildNumber || !distributed || distributed.receipt.provider !== "app-store-connect" || distributed.receipt.releaseId !== expected.releaseId || distributed.receipt.sha256 !== expected.sha256 || distributed.receipt.buildNumber !== expected.buildNumber || distributed.receipt.marketingVersion !== expected.marketingVersion || !["distributed", "review-submitted"].includes(distributed.receipt.stage) || JSON.stringify([...distributed.receipt.intent.groups].sort()) !== JSON.stringify([...options.appStoreConnect.groups ?? []].sort()) || distributed.receipt.intent.submitForReview !== (options.appStoreConnect.submitForReview ?? false) || typeof distributed.reused !== "boolean") {
15126
+ throw new TypeError("Native release publisher did not complete the requested App Store Connect release.");
15127
+ }
15128
+ }
15129
+ return publication;
15130
+ };
15131
+ var init_releasePublisher = () => {};
15132
+
14710
15133
  // src/cli/scripts/mobile.ts
14711
15134
  var exports_mobile = {};
14712
15135
  __export(exports_mobile, {
14713
15136
  runMobile: () => runMobile
14714
15137
  });
14715
- import { access as access8, mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
14716
- import { join as join43, resolve as resolve36 } from "path";
15138
+ import { access as access10, mkdir as mkdir10, writeFile as writeFile10 } from "fs/promises";
15139
+ import { join as join44, resolve as resolve38 } from "path";
14717
15140
  import { createInterface } from "readline/promises";
14718
15141
  var NOT_FOUND2 = -1, CAPACITOR_PACKAGES, valueAfter = (args, flag) => {
14719
15142
  const index = args.indexOf(flag);
@@ -14727,9 +15150,9 @@ var NOT_FOUND2 = -1, CAPACITOR_PACKAGES, valueAfter = (args, flag) => {
14727
15150
  }
14728
15151
  return value;
14729
15152
  }, capacitorExecutable = async (projectRoot) => {
14730
- const executable = join43(projectRoot, "node_modules", ".bin", "cap");
15153
+ const executable = join44(projectRoot, "node_modules", ".bin", "cap");
14731
15154
  try {
14732
- await access8(executable);
15155
+ await access10(executable);
14733
15156
  return executable;
14734
15157
  } catch {
14735
15158
  throw new TypeError(`Capacitor is not installed in this app. Run: bun add ${CAPACITOR_PACKAGES.join(" ")}`);
@@ -14770,7 +15193,7 @@ var NOT_FOUND2 = -1, CAPACITOR_PACKAGES, valueAfter = (args, flag) => {
14770
15193
  await applyAbsoluteNativeDeepLinks(mobile, platforms);
14771
15194
  }, associations = async (args) => {
14772
15195
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
14773
- const outputDirectory = resolve36(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
15196
+ const outputDirectory = resolve38(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
14774
15197
  if (args.includes("--verify")) {
14775
15198
  const result2 = await verifyAbsoluteMobileAssociationFiles(mobile);
14776
15199
  console.log(`Verified ${result2.results.length} hosted association files`);
@@ -14830,7 +15253,21 @@ Mobile release transport checks failed.`);
14830
15253
  throw new TypeError("Mobile release validation failed. Resolve every failed check before signing or publishing the app.");
14831
15254
  }
14832
15255
  }, mobileBuildServerEntry = (args) => {
14833
- const valueFlags = new Set(["--config", "--outdir", "--web-outdir"]);
15256
+ const valueFlags = new Set([
15257
+ "--channel",
15258
+ "--config",
15259
+ "--outdir",
15260
+ "--play-name",
15261
+ "--play-notes",
15262
+ "--play-rollout",
15263
+ "--play-status",
15264
+ "--play-track",
15265
+ "--play-update-priority",
15266
+ "--registry",
15267
+ "--testflight-group",
15268
+ "--testflight-notes",
15269
+ "--web-outdir"
15270
+ ]);
14834
15271
  const skipped = new Set;
14835
15272
  args.forEach((value, index) => {
14836
15273
  if (valueFlags.has(value)) {
@@ -14839,6 +15276,111 @@ Mobile release transport checks failed.`);
14839
15276
  }
14840
15277
  });
14841
15278
  return args.find((value, index) => !skipped.has(index) && value !== "--unsigned" && !value.startsWith("-")) ?? DEFAULT_SERVER_ENTRY;
15279
+ }, requireValueAfter = (args, flag) => {
15280
+ const value = valueAfter(args, flag);
15281
+ if (!value || value.startsWith("-")) {
15282
+ throw new TypeError(`mobile publish android requires ${flag} <value>.`);
15283
+ }
15284
+ return value;
15285
+ }, requireIosValueAfter = (args, flag) => {
15286
+ const value = valueAfter(args, flag);
15287
+ if (!value || value.startsWith("-")) {
15288
+ throw new TypeError(`mobile publish ios requires ${flag} <value>.`);
15289
+ }
15290
+ return value;
15291
+ }, appStoreConnectTarget = (args) => {
15292
+ const testFlightFlags = args.filter((value) => value.startsWith("--testflight-"));
15293
+ if (testFlightFlags.length === 0)
15294
+ return;
15295
+ if (args.some((value, index) => (value === "--testflight-group" || value === "--testflight-notes") && (!args[index + 1] || args[index + 1]?.startsWith("-")))) {
15296
+ throw new TypeError("mobile publish ios requires a value after every TestFlight group or notes flag.");
15297
+ }
15298
+ const groups = valuesAfter(args, "--testflight-group");
15299
+ const submitForReview = args.includes("--testflight-submit-review");
15300
+ if (submitForReview && groups.length === 0) {
15301
+ throw new TypeError("mobile publish ios --testflight-submit-review requires at least one --testflight-group.");
15302
+ }
15303
+ const whatsNew = valuesAfter(args, "--testflight-notes").map((note) => {
15304
+ const separator = note.indexOf("=");
15305
+ if (separator < 1 || separator === note.length - 1) {
15306
+ throw new TypeError("mobile publish ios --testflight-notes must use locale=text.");
15307
+ }
15308
+ return {
15309
+ locale: note.slice(0, separator),
15310
+ text: note.slice(separator + 1)
15311
+ };
15312
+ });
15313
+ return {
15314
+ ...groups.length === 0 ? {} : { groups },
15315
+ submitForReview,
15316
+ ...whatsNew.length === 0 ? {} : { whatsNew }
15317
+ };
15318
+ }, googlePlayTarget = (args) => {
15319
+ const playFlags = args.filter((value) => value.startsWith("--play-"));
15320
+ if (playFlags.length === 0)
15321
+ return;
15322
+ if (!args.includes("--play-track")) {
15323
+ throw new TypeError("mobile publish android requires --play-track <track> when using Google Play options.");
15324
+ }
15325
+ const track = requireValueAfter(args, "--play-track");
15326
+ const rolloutValue = args.includes("--play-rollout") ? requireValueAfter(args, "--play-rollout") : undefined;
15327
+ const userFraction = rolloutValue === undefined ? undefined : Number(rolloutValue);
15328
+ if (userFraction !== undefined && (!Number.isFinite(userFraction) || userFraction <= 0 || userFraction >= 1)) {
15329
+ throw new TypeError("mobile publish android --play-rollout must be greater than 0 and less than 1.");
15330
+ }
15331
+ const requestedStatus = args.includes("--play-status") ? requireValueAfter(args, "--play-status") : undefined;
15332
+ const statuses = {
15333
+ completed: "completed",
15334
+ draft: "draft",
15335
+ halted: "halted",
15336
+ "in-progress": "inProgress"
15337
+ };
15338
+ if (requestedStatus !== undefined && !(requestedStatus in statuses)) {
15339
+ throw new TypeError("mobile publish android --play-status must be completed, draft, halted, or in-progress.");
15340
+ }
15341
+ let status2;
15342
+ if (requestedStatus === undefined) {
15343
+ status2 = userFraction === undefined ? "completed" : "inProgress";
15344
+ } else if (requestedStatus === "in-progress") {
15345
+ status2 = statuses["in-progress"];
15346
+ } else if (requestedStatus === "completed") {
15347
+ status2 = statuses.completed;
15348
+ } else if (requestedStatus === "draft") {
15349
+ status2 = statuses.draft;
15350
+ } else {
15351
+ status2 = statuses.halted;
15352
+ }
15353
+ if (userFraction === undefined ? status2 === "inProgress" || status2 === "halted" : status2 !== "inProgress" && status2 !== "halted") {
15354
+ throw new TypeError("mobile publish android staged statuses require --play-rollout, and other statuses forbid it.");
15355
+ }
15356
+ const priorityValue = args.includes("--play-update-priority") ? requireValueAfter(args, "--play-update-priority") : undefined;
15357
+ const inAppUpdatePriority = priorityValue === undefined ? undefined : Number(priorityValue);
15358
+ if (inAppUpdatePriority !== undefined && (!Number.isInteger(inAppUpdatePriority) || inAppUpdatePriority < 0 || inAppUpdatePriority > 5)) {
15359
+ throw new TypeError("mobile publish android --play-update-priority must be an integer from 0 through 5.");
15360
+ }
15361
+ if (args.some((value, index) => value === "--play-notes" && (!args[index + 1] || args[index + 1]?.startsWith("-")))) {
15362
+ throw new TypeError("mobile publish android requires --play-notes <language=text>.");
15363
+ }
15364
+ const releaseNotes = valuesAfter(args, "--play-notes").map((note) => {
15365
+ const separator = note.indexOf("=");
15366
+ if (separator < 1 || separator === note.length - 1) {
15367
+ throw new TypeError("mobile publish android --play-notes must use language=text.");
15368
+ }
15369
+ return {
15370
+ language: note.slice(0, separator),
15371
+ text: note.slice(separator + 1)
15372
+ };
15373
+ });
15374
+ return {
15375
+ changesNotSentForReview: args.includes("--play-hold-review"),
15376
+ ...inAppUpdatePriority === undefined ? {} : { inAppUpdatePriority },
15377
+ ...args.includes("--play-name") ? { name: requireValueAfter(args, "--play-name") } : {},
15378
+ ...releaseNotes.length === 0 ? {} : { releaseNotes },
15379
+ reviewBehavior: args.includes("--play-cancel-existing-review") ? "CANCEL_IN_REVIEW_AND_SUBMIT" : "ERROR_IF_IN_REVIEW",
15380
+ status: status2,
15381
+ track,
15382
+ ...userFraction === undefined ? {} : { userFraction }
15383
+ };
14842
15384
  }, requireAndroidReleaseReady = async (mobile, projectRoot) => {
14843
15385
  const releaseCheck = await inspectAbsoluteMobileRelease({ ...mobile, platforms: ["android"] }, projectRoot);
14844
15386
  if (releaseCheck.ready)
@@ -14852,7 +15394,7 @@ Mobile release transport checks failed.`);
14852
15394
  status: check2.status
14853
15395
  })));
14854
15396
  throw new TypeError("Android release validation failed before Gradle signing.");
14855
- }, buildAndroid = async (args) => {
15397
+ }, buildAndroid = async (args, prepareVersionCode) => {
14856
15398
  const configPath2 = valueAfter(args, "--config");
14857
15399
  const { mobile, projectRoot } = await loadMobile(configPath2);
14858
15400
  if (!mobile.platforms.includes("android")) {
@@ -14871,13 +15413,14 @@ Mobile release transport checks failed.`);
14871
15413
  allowUnsigned: args.includes("--unsigned"),
14872
15414
  config: mobile,
14873
15415
  outputDirectory: valueAfter(args, "--outdir"),
14874
- projectRoot
15416
+ projectRoot,
15417
+ ...prepareVersionCode === undefined ? {} : { prepareVersionCode }
14875
15418
  });
14876
15419
  success = true;
14877
15420
  const durationMs = Math.round(performance.now() - startedAt);
14878
15421
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
14879
15422
  console.log(`Artifact: ${release.artifactPath}`);
14880
- console.log(`Metadata: ${join43(release.releaseRoot, "release.json")}`);
15423
+ console.log(`Metadata: ${join44(release.releaseRoot, "release.json")}`);
14881
15424
  return release;
14882
15425
  } finally {
14883
15426
  sendTelemetryEvent("mobile:android-release-build", {
@@ -14889,6 +15432,154 @@ Mobile release transport checks failed.`);
14889
15432
  unsignedAllowed: args.includes("--unsigned")
14890
15433
  });
14891
15434
  }
15435
+ }, printGooglePlayPublication = (googlePlay) => {
15436
+ console.log(`${googlePlay.reused ? "Reused" : "Committed"} Google Play version ${googlePlay.receipt.versionCode} on ${googlePlay.receipt.intent.track}.`);
15437
+ }, printAppStoreConnectPublication = (publication) => {
15438
+ const { receipt } = publication;
15439
+ console.log(`${publication.reused ? "Reused" : "Uploaded"} App Store Connect build ${receipt.marketingVersion} (${receipt.buildNumber}); ${receipt.stage}.`);
15440
+ }, publishAndroid = async (args) => {
15441
+ const registryModule = args.includes("--registry") ? requireValueAfter(args, "--registry") : "mobile.release.ts";
15442
+ const channel = args.includes("--channel") ? requireValueAfter(args, "--channel") : undefined;
15443
+ const configPath2 = valueAfter(args, "--config");
15444
+ const googlePlay = googlePlayTarget(args);
15445
+ const { mobile, projectRoot } = await loadMobile(configPath2);
15446
+ const startedAt = performance.now();
15447
+ let reused = false;
15448
+ let success = false;
15449
+ try {
15450
+ const publisher = await loadAbsoluteNativeReleasePublisher(projectRoot, registryModule);
15451
+ const release = await buildAndroid(args, googlePlay ? (buildIdentity) => prepareAbsoluteAndroidRelease(publisher, {
15452
+ buildIdentity,
15453
+ googlePlay,
15454
+ packageName: mobile.appId
15455
+ }) : undefined);
15456
+ const publication = await publishAbsoluteAndroidRelease({
15457
+ allowUnsigned: args.includes("--unsigned"),
15458
+ channel,
15459
+ googlePlay,
15460
+ modulePath: registryModule,
15461
+ projectRoot,
15462
+ release
15463
+ });
15464
+ const { reused: publicationReused } = publication;
15465
+ reused = publicationReused;
15466
+ success = true;
15467
+ console.log(`${publication.reused ? "Reused" : "Published"} Android release ${release.metadata.releaseId}${publication.channel ? ` on ${publication.channel.channel}` : ""}.`);
15468
+ if (publication.googlePlay)
15469
+ printGooglePlayPublication(publication.googlePlay);
15470
+ return publication;
15471
+ } finally {
15472
+ sendTelemetryEvent("mobile:android-release-publish", {
15473
+ durationMs: Math.round(performance.now() - startedAt),
15474
+ engine: "capacitor",
15475
+ platform: "android",
15476
+ provider: googlePlay ? "google-play" : "registry-module",
15477
+ reused,
15478
+ success,
15479
+ type: "aab",
15480
+ unsignedAllowed: args.includes("--unsigned")
15481
+ });
15482
+ }
15483
+ }, requireIosReleaseReady = async (mobile, projectRoot) => {
15484
+ const releaseCheck = await inspectAbsoluteMobileRelease({ ...mobile, platforms: ["ios"] }, projectRoot);
15485
+ if (releaseCheck.ready)
15486
+ return;
15487
+ printDoctorChecks(releaseCheck.checks.map((check2) => ({
15488
+ id: check2.id,
15489
+ label: check2.detail,
15490
+ path: check2.path,
15491
+ platform: "ios",
15492
+ remediation: check2.remediation,
15493
+ status: check2.status
15494
+ })));
15495
+ throw new TypeError("iOS release validation failed before Xcode signing.");
15496
+ }, buildIos = async (args, prepareBuildNumber) => {
15497
+ const configPath2 = valueAfter(args, "--config");
15498
+ const { mobile, projectRoot } = await loadMobile(configPath2);
15499
+ if (!mobile.platforms.includes("ios")) {
15500
+ throw new TypeError("mobile build ios requires ios in mobile.platforms.");
15501
+ }
15502
+ const startedAt = performance.now();
15503
+ let success = false;
15504
+ try {
15505
+ await start(mobileBuildServerEntry(args), valueAfter(args, "--web-outdir"), configPath2, { prepareOnly: true });
15506
+ await writeAbsoluteCapacitorConfig(mobile, { projectRoot });
15507
+ await runCapacitorForPlatforms(projectRoot, "sync", ["ios"]);
15508
+ await applyAbsoluteNativeDeepLinks(mobile, ["ios"]);
15509
+ await requireIosReleaseReady(mobile, projectRoot);
15510
+ const release = await buildAbsoluteIosRelease({
15511
+ allowUnsigned: args.includes("--unsigned"),
15512
+ config: mobile,
15513
+ outputDirectory: valueAfter(args, "--outdir"),
15514
+ ...prepareBuildNumber === undefined ? {} : { prepareBuildNumber },
15515
+ projectRoot
15516
+ });
15517
+ success = true;
15518
+ const durationMs = Math.round(performance.now() - startedAt);
15519
+ console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} iOS IPA ${release.metadata.marketingVersion}${release.metadata.buildNumber ? ` (${release.metadata.buildNumber})` : ""} in ${getDurationString(durationMs)}.`);
15520
+ console.log(`Artifact: ${release.artifactPath}`);
15521
+ console.log(`Metadata: ${join44(release.releaseRoot, "release.json")}`);
15522
+ return release;
15523
+ } finally {
15524
+ sendTelemetryEvent("mobile:ios-release-build", {
15525
+ durationMs: Math.round(performance.now() - startedAt),
15526
+ engine: "capacitor",
15527
+ platform: "ios",
15528
+ success,
15529
+ type: "ipa",
15530
+ unsignedAllowed: args.includes("--unsigned")
15531
+ });
15532
+ }
15533
+ }, publishIos = async (args) => {
15534
+ const registryModule = args.includes("--registry") ? requireIosValueAfter(args, "--registry") : "mobile.release.ts";
15535
+ const channel = args.includes("--channel") ? requireIosValueAfter(args, "--channel") : undefined;
15536
+ const configPath2 = valueAfter(args, "--config");
15537
+ const appStoreConnect = appStoreConnectTarget(args);
15538
+ const { mobile, projectRoot } = await loadMobile(configPath2);
15539
+ const startedAt = performance.now();
15540
+ let reused = false;
15541
+ let success = false;
15542
+ try {
15543
+ const publisher = await loadAbsoluteNativeReleasePublisher(projectRoot, registryModule);
15544
+ const release = await buildIos(args, appStoreConnect ? (buildIdentity) => {
15545
+ if (!mobile.iosVersion)
15546
+ throw new TypeError("iOS publishing requires mobile.ios.version.");
15547
+ return prepareAbsoluteIosRelease(publisher, {
15548
+ buildIdentity,
15549
+ bundleId: mobile.appId,
15550
+ marketingVersion: mobile.iosVersion
15551
+ });
15552
+ } : undefined);
15553
+ const publication = await publishAbsoluteIosRelease({
15554
+ allowUnsigned: args.includes("--unsigned"),
15555
+ appStoreConnect,
15556
+ channel,
15557
+ modulePath: registryModule,
15558
+ projectRoot,
15559
+ release
15560
+ });
15561
+ const {
15562
+ appStoreConnect: appStoreConnectPublication,
15563
+ reused: publicationReused
15564
+ } = publication;
15565
+ reused = publicationReused;
15566
+ success = true;
15567
+ console.log(`${publication.reused ? "Reused" : "Published"} iOS release ${release.metadata.releaseId}${publication.channel ? ` on ${publication.channel.channel}` : ""}.`);
15568
+ if (appStoreConnectPublication)
15569
+ printAppStoreConnectPublication(appStoreConnectPublication);
15570
+ return publication;
15571
+ } finally {
15572
+ sendTelemetryEvent("mobile:ios-release-publish", {
15573
+ durationMs: Math.round(performance.now() - startedAt),
15574
+ engine: "capacitor",
15575
+ platform: "ios",
15576
+ provider: appStoreConnect ? "app-store-connect" : "registry-module",
15577
+ reused,
15578
+ success,
15579
+ type: "ipa",
15580
+ unsignedAllowed: args.includes("--unsigned")
15581
+ });
15582
+ }
14892
15583
  }, doctor = async (args) => {
14893
15584
  if (args.includes("release")) {
14894
15585
  await runReleaseDoctor(args);
@@ -14955,7 +15646,7 @@ Emulator setup verification:`);
14955
15646
  }
14956
15647
  return { https: args.includes("--https"), port };
14957
15648
  }
14958
- const instances = listLiveInstances().filter((instance2) => resolve36(instance2.cwd) === resolve36(projectRoot) && instance2.source === "dev" && instance2.port !== null);
15649
+ const instances = listLiveInstances().filter((instance2) => resolve38(instance2.cwd) === resolve38(projectRoot) && instance2.source === "dev" && instance2.port !== null);
14959
15650
  if (instances.length !== 1) {
14960
15651
  throw new TypeError(instances.length === 0 ? "No running AbsoluteJS dev server was found for this project. Start `bun dev`, wait for Android to report ready, then run `absolute mobile test android`." : "Multiple dev servers are running for this project. Select one with mobile test android --port <port>.");
14961
15652
  }
@@ -14998,8 +15689,8 @@ Emulator setup verification:`);
14998
15689
  }
14999
15690
  return selected;
15000
15691
  }, safeArtifactRoot = (projectRoot, value) => {
15001
- const root = resolve36(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
15002
- if (root !== projectRoot && !root.startsWith(`${resolve36(projectRoot)}/`)) {
15692
+ const root = resolve38(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
15693
+ if (root !== projectRoot && !root.startsWith(`${resolve38(projectRoot)}/`)) {
15003
15694
  throw new TypeError("mobile test --artifacts must remain inside the project.");
15004
15695
  }
15005
15696
  return root;
@@ -15023,12 +15714,12 @@ Emulator setup verification:`);
15023
15714
  timeoutMs
15024
15715
  });
15025
15716
  }, writeAndroidFailureArtifacts = async (options) => {
15026
- await mkdir9(options.artifactRoot, { recursive: true });
15027
- const screenshot = options.session ? await options.session.screenshot(join43(options.artifactRoot, "android-failure.png")).catch(() => {
15717
+ await mkdir10(options.artifactRoot, { recursive: true });
15718
+ const screenshot = options.session ? await options.session.screenshot(join44(options.artifactRoot, "android-failure.png")).catch(() => {
15028
15719
  return;
15029
15720
  }) : undefined;
15030
- const diagnosticPath = join43(options.artifactRoot, "android-failure.json");
15031
- await writeFile9(diagnosticPath, `${JSON.stringify({
15721
+ const diagnosticPath = join44(options.artifactRoot, "android-failure.json");
15722
+ await writeFile10(diagnosticPath, `${JSON.stringify({
15032
15723
  diagnostics: options.session?.diagnostics ?? [],
15033
15724
  error: options.error instanceof Error ? options.error.message : String(options.error),
15034
15725
  platform: "android",
@@ -15138,7 +15829,19 @@ Emulator setup verification:`);
15138
15829
  await buildAndroid(args.slice(2));
15139
15830
  return;
15140
15831
  }
15141
- throw new TypeError("Usage: absolute mobile <init [--no-native] [--force] | sync [ios|android] | associations [--outdir dir] [--verify] | doctor [ios|android|release] [--json|--fix [--yes]] | build android [server-entry] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json]> [--config path]");
15832
+ if (command === "build" && args[1] === "ios") {
15833
+ await buildIos(args.slice(2));
15834
+ return;
15835
+ }
15836
+ if (command === "publish" && args[1] === "android") {
15837
+ await publishAndroid(args.slice(2));
15838
+ return;
15839
+ }
15840
+ if (command === "publish" && args[1] === "ios") {
15841
+ await publishIos(args.slice(2));
15842
+ return;
15843
+ }
15844
+ throw new TypeError("Usage: absolute mobile <init [--no-native] [--force] | sync [ios|android] | associations [--outdir dir] [--verify] | doctor [ios|android|release] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--outdir dir] [--web-outdir dir] [--unsigned] | publish android [server-entry] [--registry module] [--channel name] [--play-track track] [--play-status completed|draft|halted|in-progress] [--play-rollout fraction] [--play-name name] [--play-notes language=text] [--play-update-priority 0..5] [--play-hold-review] [--play-cancel-existing-review] [--outdir dir] [--web-outdir dir] [--unsigned] | publish ios [server-entry] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json]> [--config path]");
15142
15845
  };
15143
15846
  var init_mobile = __esm(() => {
15144
15847
  init_capacitorProject();
@@ -15154,6 +15857,8 @@ var init_mobile = __esm(() => {
15154
15857
  init_telemetryEvent();
15155
15858
  init_releaseDoctor();
15156
15859
  init_androidRelease();
15860
+ init_iosRelease();
15861
+ init_releasePublisher();
15157
15862
  init_start();
15158
15863
  init_utils();
15159
15864
  init_getDurationString();
@@ -15171,10 +15876,10 @@ var exports_typecheck = {};
15171
15876
  __export(exports_typecheck, {
15172
15877
  typecheck: () => typecheck
15173
15878
  });
15174
- import { resolve as resolve37, join as join44 } from "path";
15879
+ import { resolve as resolve39, join as join45 } from "path";
15175
15880
  import { existsSync as existsSync42, readFileSync as readFileSync37 } from "fs";
15176
- import { mkdir as mkdir10, writeFile as writeFile10 } from "fs/promises";
15177
- var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve37(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
15881
+ import { mkdir as mkdir11, writeFile as writeFile11 } from "fs/promises";
15882
+ var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve39(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
15178
15883
  if (!existsSync42(resolveConfigPath(configPath2))) {
15179
15884
  const defaultService = {};
15180
15885
  return [defaultService];
@@ -15196,7 +15901,7 @@ var isCommandService3 = (service) => service.kind === "command" || Array.isArray
15196
15901
  const exitCode = await proc.exited;
15197
15902
  return { exitCode, name, output: (stdout + stderr).trim() };
15198
15903
  }, shellEscape = (value) => `'${value.replaceAll("'", "'\\''")}'`, runShell = async (name, command) => run(name, ["/bin/bash", "-lc", command]), findBin = (name) => {
15199
- const local = resolve37("node_modules", ".bin", name);
15904
+ const local = resolve39("node_modules", ".bin", name);
15200
15905
  return existsSync42(local) ? local : null;
15201
15906
  }, ANSI_COLOR_REGEX, ANSI_PURPLE_REGEX, ANSI_CYAN_REGEX, ANSI_TOKEN_END_REGEX, stripAnsi4 = (str) => str.replace(ANSI_COLOR_REGEX, ""), formatSvelteOutput = (output) => {
15202
15907
  const cwd = `${process.cwd()}/`;
@@ -15244,15 +15949,15 @@ Found ${errorCount} error${suffix}.`;
15244
15949
  return formatted;
15245
15950
  }, ABSOLUTE_INTERNAL_EXCLUDES, resolveAbsoluteTypeFile = (fileName) => {
15246
15951
  const candidates = [
15247
- resolve37("node_modules/@absolutejs/absolute/dist/types", fileName),
15248
- resolve37(import.meta.dir, "../types", fileName),
15249
- resolve37(import.meta.dir, "../../types", fileName),
15250
- resolve37(import.meta.dir, "../../../types", fileName)
15952
+ resolve39("node_modules/@absolutejs/absolute/dist/types", fileName),
15953
+ resolve39(import.meta.dir, "../types", fileName),
15954
+ resolve39(import.meta.dir, "../../types", fileName),
15955
+ resolve39(import.meta.dir, "../../../types", fileName)
15251
15956
  ];
15252
15957
  return candidates.find((candidate) => existsSync42(candidate)) ?? candidates[0];
15253
15958
  }, ABSOLUTE_TYPECHECK_FILES, readProjectTsconfig = () => {
15254
15959
  try {
15255
- return JSON.parse(readFileSync37(resolve37("tsconfig.json"), "utf-8"));
15960
+ return JSON.parse(readFileSync37(resolve39("tsconfig.json"), "utf-8"));
15256
15961
  } catch {
15257
15962
  return {};
15258
15963
  }
@@ -15280,22 +15985,22 @@ Found ${errorCount} error${suffix}.`;
15280
15985
  console.error("\x1B[31m\u2717\x1B[0m vue-tsc is required for Vue type checking. Install it: bun add -d vue-tsc");
15281
15986
  process.exit(1);
15282
15987
  }
15283
- const vueTsconfigPath = join44(cacheDir, "tsconfig.vue-check.json");
15284
- return writeFile10(vueTsconfigPath, JSON.stringify({
15988
+ const vueTsconfigPath = join45(cacheDir, "tsconfig.vue-check.json");
15989
+ return writeFile11(vueTsconfigPath, JSON.stringify({
15285
15990
  compilerOptions: {
15286
15991
  rootDir: ".."
15287
15992
  },
15288
15993
  exclude: getProjectTypecheckExcludes(),
15289
- extends: resolve37("tsconfig.json"),
15994
+ extends: resolve39("tsconfig.json"),
15290
15995
  include: getProjectTypecheckIncludes()
15291
15996
  }, null, "\t")).then(() => run("vue-tsc", [
15292
15997
  vueTscBin,
15293
15998
  "--noEmit",
15294
15999
  "--project",
15295
- resolve37(vueTsconfigPath),
16000
+ resolve39(vueTsconfigPath),
15296
16001
  "--incremental",
15297
16002
  "--tsBuildInfoFile",
15298
- join44(cacheDir, "vue-tsc.tsbuildinfo"),
16003
+ join45(cacheDir, "vue-tsc.tsbuildinfo"),
15299
16004
  "--pretty"
15300
16005
  ]));
15301
16006
  }, buildAngularCheck = async (cacheDir, angularDir) => {
@@ -15304,8 +16009,8 @@ Found ${errorCount} error${suffix}.`;
15304
16009
  console.error("\x1B[31m\u2717\x1B[0m @angular/compiler-cli is required for Angular type checking. Install it: bun add -d @angular/compiler-cli");
15305
16010
  process.exit(1);
15306
16011
  }
15307
- const angularTsconfigPath = join44(cacheDir, "tsconfig.angular-check.json");
15308
- await writeFile10(angularTsconfigPath, JSON.stringify({
16012
+ const angularTsconfigPath = join45(cacheDir, "tsconfig.angular-check.json");
16013
+ await writeFile11(angularTsconfigPath, JSON.stringify({
15309
16014
  angularCompilerOptions: {
15310
16015
  strictTemplates: true
15311
16016
  },
@@ -15314,32 +16019,32 @@ Found ${errorCount} error${suffix}.`;
15314
16019
  rootDir: ".."
15315
16020
  },
15316
16021
  exclude: ABSOLUTE_INTERNAL_EXCLUDES.map(toGeneratedConfigPath),
15317
- extends: resolve37("tsconfig.json"),
16022
+ extends: resolve39("tsconfig.json"),
15318
16023
  include: [`../${angularDir}/**/*`]
15319
16024
  }, null, "\t"));
15320
- return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve37(angularTsconfigPath))}`);
16025
+ return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve39(angularTsconfigPath))}`);
15321
16026
  }, buildTscCheck = (cacheDir) => {
15322
16027
  const tscBin = findBin("tsc");
15323
16028
  if (!tscBin) {
15324
16029
  console.error("\x1B[31m\u2717\x1B[0m typescript is required for type checking. Install it: bun add -d typescript");
15325
16030
  process.exit(1);
15326
16031
  }
15327
- const tscConfigPath = join44(cacheDir, "tsconfig.typecheck.json");
15328
- return writeFile10(tscConfigPath, JSON.stringify({
16032
+ const tscConfigPath = join45(cacheDir, "tsconfig.typecheck.json");
16033
+ return writeFile11(tscConfigPath, JSON.stringify({
15329
16034
  compilerOptions: {
15330
16035
  rootDir: ".."
15331
16036
  },
15332
16037
  exclude: getProjectTypecheckExcludes(),
15333
- extends: resolve37("tsconfig.json"),
16038
+ extends: resolve39("tsconfig.json"),
15334
16039
  include: getProjectTypecheckIncludes()
15335
16040
  }, null, "\t")).then(() => run("tsc", [
15336
16041
  tscBin,
15337
16042
  "--noEmit",
15338
16043
  "--project",
15339
- resolve37(tscConfigPath),
16044
+ resolve39(tscConfigPath),
15340
16045
  "--incremental",
15341
16046
  "--tsBuildInfoFile",
15342
- join44(cacheDir, "tsc.tsbuildinfo"),
16047
+ join45(cacheDir, "tsc.tsbuildinfo"),
15343
16048
  "--pretty"
15344
16049
  ]));
15345
16050
  }, buildSvelteCheck = async (cacheDir, svelteDir) => {
@@ -15348,16 +16053,16 @@ Found ${errorCount} error${suffix}.`;
15348
16053
  console.error("\x1B[31m\u2717\x1B[0m svelte-check is required for Svelte type checking. Install it: bun add -d svelte-check");
15349
16054
  process.exit(1);
15350
16055
  }
15351
- const svelteTsconfigPath = join44(cacheDir, "tsconfig.svelte-check.json");
15352
- await writeFile10(svelteTsconfigPath, JSON.stringify({
15353
- extends: resolve37("tsconfig.json"),
16056
+ const svelteTsconfigPath = join45(cacheDir, "tsconfig.svelte-check.json");
16057
+ await writeFile11(svelteTsconfigPath, JSON.stringify({
16058
+ extends: resolve39("tsconfig.json"),
15354
16059
  files: ABSOLUTE_TYPECHECK_FILES,
15355
16060
  include: [`../${svelteDir}/**/*`]
15356
16061
  }, null, "\t"));
15357
16062
  return run("svelte-check", [
15358
16063
  svelteBin,
15359
16064
  "--tsconfig",
15360
- resolve37(svelteTsconfigPath),
16065
+ resolve39(svelteTsconfigPath),
15361
16066
  "--threshold",
15362
16067
  "error",
15363
16068
  "--compiler-warnings",
@@ -15378,7 +16083,7 @@ Found ${errorCount} error${suffix}.`;
15378
16083
  ...new Set(targets.map((config) => config.angularDirectory).filter((dir) => typeof dir === "string" && dir.length > 0))
15379
16084
  ];
15380
16085
  const cacheDir = ".absolutejs";
15381
- await mkdir10(cacheDir, { recursive: true });
16086
+ await mkdir11(cacheDir, { recursive: true });
15382
16087
  const checks = [];
15383
16088
  checks.push(hasVue ? buildVueTscCheck(cacheDir) : buildTscCheck(cacheDir));
15384
16089
  for (const svelteDir of hasSvelte ? svelteDirs : []) {
@@ -15551,11 +16256,11 @@ var DEFAULT_RELAY_PORT = 8787, DEFAULT_REQUEST_TIMEOUT_MS = 30000, headersToObje
15551
16256
  url: url.pathname + url.search,
15552
16257
  ...bodyBytes && bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
15553
16258
  };
15554
- const responsePromise = new Promise((resolve38) => {
15555
- pending.set(id, resolve38);
16259
+ const responsePromise = new Promise((resolve40) => {
16260
+ pending.set(id, resolve40);
15556
16261
  });
15557
16262
  client.send(encodeTunnelMessage(message));
15558
- const timeout = new Promise((resolve38) => setTimeout(() => resolve38({ id, message: "timeout", type: "error" }), requestTimeoutMs));
16263
+ const timeout = new Promise((resolve40) => setTimeout(() => resolve40({ id, message: "timeout", type: "error" }), requestTimeoutMs));
15559
16264
  const result = await Promise.race([responsePromise, timeout]);
15560
16265
  pending.delete(id);
15561
16266
  if (result.type === "error") {