@absolutejs/absolute 0.20.0-beta.21 → 0.20.0-beta.23

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
@@ -6763,7 +6763,7 @@ var init_syncSchema = __esm(() => {
6763
6763
  import { readFileSync as readFileSync12 } from "fs";
6764
6764
  import { extname as extname5, join as join20, relative as relative11, resolve as resolve16 } from "path";
6765
6765
  import ts4 from "typescript";
6766
- var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/devices-capacitor", SOURCE_GLOB, IGNORED_DIRECTORIES, IDENTIFIER_PATTERN, CAPACITOR_MODULE_PATTERN, CAPACITOR_PACKAGE_PATTERN, ANDROID_PERMISSION_PATTERN, IOS_USAGE_DESCRIPTIONS, object2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), readJson = (path) => {
6766
+ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/devices-capacitor", SOURCE_GLOB, IGNORED_DIRECTORIES, IDENTIFIER_PATTERN, CAPACITOR_MODULE_PATTERN, CAPACITOR_PACKAGE_PATTERN, ANDROID_PERMISSION_PATTERN, IOS_USAGE_DESCRIPTIONS, IOS_PRIVACY_ACCESSED_API_REASONS, IOS_PRIVACY_ACCESSED_APIS, object2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), readJson = (path) => {
6767
6767
  const value = JSON.parse(readFileSync12(path, "utf8"));
6768
6768
  if (!object2(value))
6769
6769
  throw new TypeError(`${path} must contain an object.`);
@@ -6781,15 +6781,37 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
6781
6781
  if (!Array.isArray(permissions) || !permissions.every((permission) => typeof permission === "string" && ANDROID_PERMISSION_PATTERN.test(permission)))
6782
6782
  throw new TypeError(`${field}.permissions must contain Android permission names.`);
6783
6783
  return [...permissions];
6784
- }, iosUsageDescriptions = (value, field) => {
6784
+ }, iosPrivacyAccessedApis = (value, field) => {
6785
6785
  if (value === undefined)
6786
6786
  return;
6787
6787
  if (!object2(value))
6788
6788
  throw new TypeError(`${field} must be an object.`);
6789
- const { usageDescriptions } = value;
6790
- if (!Array.isArray(usageDescriptions) || !usageDescriptions.every((purpose) => typeof purpose === "string" && IOS_USAGE_DESCRIPTIONS.has(purpose)))
6789
+ const privacy = {};
6790
+ for (const api of IOS_PRIVACY_ACCESSED_APIS) {
6791
+ const reasons = value[api];
6792
+ if (reasons === undefined)
6793
+ continue;
6794
+ const supported = IOS_PRIVACY_ACCESSED_API_REASONS[api];
6795
+ if (!Array.isArray(reasons) || reasons.length === 0 || !reasons.every((reason) => typeof reason === "string" && supported.has(reason)))
6796
+ throw new TypeError(`${field} contains an unsupported API or reason.`);
6797
+ privacy[api] = [...reasons];
6798
+ }
6799
+ if (Object.keys(value).some((api) => !IOS_PRIVACY_ACCESSED_APIS.some((known) => known === api)))
6800
+ throw new TypeError(`${field} contains an unsupported API or reason.`);
6801
+ return privacy;
6802
+ }, iosNativeRequirements = (value, field) => {
6803
+ if (value === undefined)
6804
+ return;
6805
+ if (!object2(value))
6806
+ throw new TypeError(`${field} must be an object.`);
6807
+ const { privacyAccessedApis, usageDescriptions } = value;
6808
+ if (usageDescriptions !== undefined && (!Array.isArray(usageDescriptions) || !usageDescriptions.every((purpose) => typeof purpose === "string" && IOS_USAGE_DESCRIPTIONS.has(purpose))))
6791
6809
  throw new TypeError(`${field}.usageDescriptions contains an unsupported purpose.`);
6792
- return [...usageDescriptions];
6810
+ const privacy = iosPrivacyAccessedApis(privacyAccessedApis, `${field}.privacyAccessedApis`);
6811
+ return {
6812
+ ...privacy === undefined ? {} : { privacyAccessedApis: privacy },
6813
+ ...usageDescriptions === undefined ? {} : { usageDescriptions: [...usageDescriptions] }
6814
+ };
6793
6815
  }, parseProvider = (name, value) => {
6794
6816
  if (!IDENTIFIER_PATTERN.test(name))
6795
6817
  throw new TypeError("Device capability names must be identifiers.");
@@ -6810,10 +6832,10 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
6810
6832
  throw new TypeError(`${name}.native must be an object.`);
6811
6833
  const { android, ios } = nativeMetadata;
6812
6834
  const permissions = androidPermissions(android, `${name}.native.android`);
6813
- const usageDescriptions = iosUsageDescriptions(ios, `${name}.native.ios`);
6835
+ const iosRequirements = iosNativeRequirements(ios, `${name}.native.ios`);
6814
6836
  native = {
6815
6837
  ...permissions === undefined ? {} : { android: { permissions } },
6816
- ...usageDescriptions === undefined ? {} : { ios: { usageDescriptions } }
6838
+ ...iosRequirements === undefined ? {} : { ios: iosRequirements }
6817
6839
  };
6818
6840
  }
6819
6841
  return {
@@ -6822,14 +6844,32 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
6822
6844
  ...native === undefined ? {} : { native },
6823
6845
  packages: [...value.packages]
6824
6846
  };
6825
- }, absoluteDeviceNativeRequirements = (plan) => ({
6826
- androidPermissions: [
6827
- ...new Set(plan.capabilities.flatMap((name) => plan.providers[name]?.native?.android?.permissions ?? []))
6828
- ].sort(),
6829
- iosUsageDescriptions: [
6830
- ...new Set(plan.capabilities.flatMap((name) => plan.providers[name]?.native?.ios?.usageDescriptions ?? []))
6831
- ].sort()
6832
- }), loadAbsoluteDeviceCapabilityProviders = (projectRoot) => {
6847
+ }, absoluteDeviceNativeRequirements = (plan) => {
6848
+ const privacy = plan.capabilities.reduce((requirements, name) => {
6849
+ for (const api of IOS_PRIVACY_ACCESSED_APIS) {
6850
+ const reasons = plan.providers[name]?.native?.ios?.privacyAccessedApis?.[api] ?? [];
6851
+ if (reasons.length === 0)
6852
+ continue;
6853
+ const current = requirements[api] ?? new Set;
6854
+ for (const reason of reasons)
6855
+ current.add(reason);
6856
+ requirements[api] = current;
6857
+ }
6858
+ return requirements;
6859
+ }, {});
6860
+ return {
6861
+ androidPermissions: [
6862
+ ...new Set(plan.capabilities.flatMap((name) => plan.providers[name]?.native?.android?.permissions ?? []))
6863
+ ].sort(),
6864
+ iosPrivacyAccessedApis: IOS_PRIVACY_ACCESSED_APIS.flatMap((api) => {
6865
+ const reasons = privacy[api];
6866
+ return reasons ? [{ api, reasons: [...reasons].sort() }] : [];
6867
+ }),
6868
+ iosUsageDescriptions: [
6869
+ ...new Set(plan.capabilities.flatMap((name) => plan.providers[name]?.native?.ios?.usageDescriptions ?? []))
6870
+ ].sort()
6871
+ };
6872
+ }, loadAbsoluteDeviceCapabilityProviders = (projectRoot) => {
6833
6873
  const path = join20(resolve16(projectRoot), "node_modules", CAPACITOR_ADAPTER, "package.json");
6834
6874
  const manifest = readJson(path);
6835
6875
  const { absolutejs } = manifest;
@@ -6953,6 +6993,12 @@ var init_deviceCapabilities = __esm(() => {
6953
6993
  "photo-library",
6954
6994
  "photo-library-add"
6955
6995
  ]);
6996
+ IOS_PRIVACY_ACCESSED_API_REASONS = {
6997
+ NSPrivacyAccessedAPICategoryFileTimestamp: new Set(["C617.1"])
6998
+ };
6999
+ IOS_PRIVACY_ACCESSED_APIS = [
7000
+ "NSPrivacyAccessedAPICategoryFileTimestamp"
7001
+ ];
6956
7002
  });
6957
7003
 
6958
7004
  // src/mobile/buildPipeline.ts
@@ -16469,7 +16515,7 @@ var init_nativeDeepLinks = () => {};
16469
16515
  // src/mobile/nativeDeviceCapabilities.ts
16470
16516
  import { readFile as readFile12, rename as rename9, writeFile as writeFile10 } from "fs/promises";
16471
16517
  import { join as join47 } from "path";
16472
- var START_MARKER2 = "<!-- absolutejs:device-capabilities:start -->", END_MARKER2 = "<!-- absolutejs:device-capabilities:end -->", NOT_FOUND2 = -1, escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"), writeChangedFile2 = async (path, source) => {
16518
+ var START_MARKER2 = "<!-- absolutejs:device-capabilities:start -->", END_MARKER2 = "<!-- absolutejs:device-capabilities:end -->", NOT_FOUND2 = -1, IOS_PRIVACY_FILE_REFERENCE = "A85D0C000000000000000001", IOS_PRIVACY_BUILD_FILE = "A85D0C000000000000000002", escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"), writeChangedFile2 = async (path, source) => {
16473
16519
  const current = await readFile12(path, "utf8");
16474
16520
  if (current === source)
16475
16521
  return false;
@@ -16477,6 +16523,14 @@ var START_MARKER2 = "<!-- absolutejs:device-capabilities:start -->", END_MARKER2
16477
16523
  await writeFile10(temporary, source, { flag: "wx" });
16478
16524
  await rename9(temporary, path);
16479
16525
  return true;
16526
+ }, optionalSource = async (path) => {
16527
+ try {
16528
+ return await readFile12(path, "utf8");
16529
+ } catch (error) {
16530
+ if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT")
16531
+ return null;
16532
+ throw error;
16533
+ }
16480
16534
  }, managed = (source, region, insertion) => {
16481
16535
  const start2 = source.indexOf(START_MARKER2);
16482
16536
  const end = source.indexOf(END_MARKER2);
@@ -16505,6 +16559,129 @@ var START_MARKER2 = "<!-- absolutejs:device-capabilities:start -->", END_MARKER2
16505
16559
  if (purpose === "location-always")
16506
16560
  return `${appName} does not track location in the background; this description supports the foreground location provider required by the native runtime.`;
16507
16561
  return `${appName} adds to your photo library only for photo actions you choose.`;
16562
+ }, privacyEntries = (requirements) => requirements.iosPrivacyAccessedApis.map(({ api, reasons }) => ` <dict>
16563
+ <key>NSPrivacyAccessedAPIType</key>
16564
+ <string>${escapeXml2(api)}</string>
16565
+ <key>NSPrivacyAccessedAPITypeReasons</key>
16566
+ <array>
16567
+ ${reasons.map((reason) => ` <string>${escapeXml2(reason)}</string>`).join(`
16568
+ `)}
16569
+ </array>
16570
+ </dict>`).join(`
16571
+ `), privacyManifestSource = (source, requirements) => {
16572
+ const entries = privacyEntries(requirements);
16573
+ const wholeRegion = entries ? ` ${START_MARKER2}
16574
+ <key>NSPrivacyAccessedAPITypes</key>
16575
+ <array>
16576
+ ${entries}
16577
+ </array>
16578
+ ${END_MARKER2}
16579
+ ` : "";
16580
+ if (source === null)
16581
+ return entries ? `<?xml version="1.0" encoding="UTF-8"?>
16582
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
16583
+ <plist version="1.0">
16584
+ <dict>
16585
+ ${wholeRegion} <key>NSPrivacyCollectedDataTypes</key>
16586
+ <array/>
16587
+ <key>NSPrivacyTracking</key>
16588
+ <false/>
16589
+ </dict>
16590
+ </plist>
16591
+ ` : null;
16592
+ const start2 = source.indexOf(START_MARKER2);
16593
+ if (start2 !== NOT_FOUND2) {
16594
+ const end = source.indexOf(END_MARKER2);
16595
+ if (end === NOT_FOUND2)
16596
+ throw new TypeError("AbsoluteJS device-capability ownership markers are malformed.");
16597
+ const owned = source.slice(start2, end);
16598
+ const ownsWholeKey = owned.includes("NSPrivacyAccessedAPITypes");
16599
+ let region = "";
16600
+ if (ownsWholeKey)
16601
+ region = wholeRegion;
16602
+ else if (entries)
16603
+ region = ` ${START_MARKER2}
16604
+ ${entries}
16605
+ ${END_MARKER2}
16606
+ `;
16607
+ return managed(source, region, source.lastIndexOf("</dict>"));
16608
+ }
16609
+ const key = source.indexOf("<key>NSPrivacyAccessedAPITypes</key>");
16610
+ if (key === NOT_FOUND2)
16611
+ return managed(source, wholeRegion, source.lastIndexOf("</dict>"));
16612
+ if (!entries)
16613
+ return source;
16614
+ const array = source.indexOf("<array>", key);
16615
+ if (array === NOT_FOUND2)
16616
+ throw new TypeError("iOS PrivacyInfo.xcprivacy has a malformed NSPrivacyAccessedAPITypes value.");
16617
+ const insertion = source.indexOf(`
16618
+ `, array);
16619
+ if (insertion === NOT_FOUND2)
16620
+ throw new TypeError("iOS PrivacyInfo.xcprivacy array is malformed.");
16621
+ return managed(source, ` ${START_MARKER2}
16622
+ ${entries}
16623
+ ${END_MARKER2}
16624
+ `, insertion + 1);
16625
+ }, writeIosPrivacyManifest = async (path, current, source) => {
16626
+ if (source === null)
16627
+ return false;
16628
+ if (current !== null)
16629
+ return writeChangedFile2(path, source);
16630
+ await writeFile10(path, source, { flag: "wx" });
16631
+ return true;
16632
+ }, configureIosPrivacyProject = async (config, requirements) => {
16633
+ if (requirements.iosPrivacyAccessedApis.length === 0)
16634
+ return false;
16635
+ const projectPath = join47(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
16636
+ const project = await readFile12(projectPath, "utf8");
16637
+ return writeChangedFile2(projectPath, addIosPrivacyProjectReference(project));
16638
+ }, addIosPrivacyProjectReference = (source) => {
16639
+ const fileMatch = source.match(/([A-F0-9]{24}) \/\* PrivacyInfo\.xcprivacy \*\/ = \{isa = PBXFileReference;/u);
16640
+ const fileReference = fileMatch?.[1] ?? IOS_PRIVACY_FILE_REFERENCE;
16641
+ const buildMatch = source.match(/([A-F0-9]{24}) \/\* PrivacyInfo\.xcprivacy in Resources \*\/ = \{isa = PBXBuildFile;/u);
16642
+ const buildFile = buildMatch?.[1] ?? IOS_PRIVACY_BUILD_FILE;
16643
+ if (!fileMatch && source.includes(fileReference) || !buildMatch && source.includes(buildFile))
16644
+ throw new TypeError("AbsoluteJS iOS privacy-manifest identifiers collide.");
16645
+ let next = source;
16646
+ if (!buildMatch) {
16647
+ const marker = "/* End PBXBuildFile section */";
16648
+ const index = next.indexOf(marker);
16649
+ if (index === NOT_FOUND2)
16650
+ throw new TypeError("Could not find the iOS PBXBuildFile section.");
16651
+ next = `${next.slice(0, index)} ${buildFile} /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = ${fileReference} /* PrivacyInfo.xcprivacy */; };
16652
+ ${next.slice(index)}`;
16653
+ }
16654
+ if (!fileMatch) {
16655
+ const marker = "/* End PBXFileReference section */";
16656
+ const index = next.indexOf(marker);
16657
+ if (index === NOT_FOUND2)
16658
+ throw new TypeError("Could not find the iOS PBXFileReference section.");
16659
+ next = `${next.slice(0, index)} ${fileReference} /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
16660
+ ${next.slice(index)}`;
16661
+ }
16662
+ const groupsStart = next.indexOf("/* Begin PBXGroup section */");
16663
+ const groupsEnd = next.indexOf("/* End PBXGroup section */");
16664
+ const groups = next.slice(groupsStart, groupsEnd);
16665
+ if (!groups.includes(`${fileReference} /* PrivacyInfo.xcprivacy */`)) {
16666
+ const appGroup = groups.match(/[A-F0-9]{24} \/\* App \*\/ = \{\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = \(\n/u);
16667
+ if (!appGroup || appGroup.index === undefined)
16668
+ throw new TypeError("Could not find the iOS App PBXGroup.");
16669
+ const index = groupsStart + appGroup.index + appGroup[0].length;
16670
+ next = `${next.slice(0, index)} ${fileReference} /* PrivacyInfo.xcprivacy */,
16671
+ ${next.slice(index)}`;
16672
+ }
16673
+ const resourcesStart = next.indexOf("/* Begin PBXResourcesBuildPhase section */");
16674
+ const resourcesEnd = next.indexOf("/* End PBXResourcesBuildPhase section */");
16675
+ const resources = next.slice(resourcesStart, resourcesEnd);
16676
+ if (!resources.includes(`${buildFile} /* PrivacyInfo.xcprivacy in Resources */`)) {
16677
+ const files = resources.match(/isa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = \d+;\n\t\t\tfiles = \(\n/u);
16678
+ if (!files || files.index === undefined)
16679
+ throw new TypeError("Could not find the iOS Resources build phase.");
16680
+ const index = resourcesStart + files.index + files[0].length;
16681
+ next = `${next.slice(0, index)} ${buildFile} /* PrivacyInfo.xcprivacy in Resources */,
16682
+ ${next.slice(index)}`;
16683
+ }
16684
+ return next;
16508
16685
  }, configureIos2 = async (config, plan) => {
16509
16686
  const path = join47(config.nativeProjectDirectory, "ios/App/App/Info.plist");
16510
16687
  const source = await readFile12(path, "utf8");
@@ -16516,7 +16693,15 @@ var START_MARKER2 = "<!-- absolutejs:device-capabilities:start -->", END_MARKER2
16516
16693
  ${content}
16517
16694
  ${END_MARKER2}
16518
16695
  ` : "";
16519
- return writeChangedFile2(path, managed(source, region, source.lastIndexOf("</dict>")));
16696
+ const infoChanged = await writeChangedFile2(path, managed(source, region, source.lastIndexOf("</dict>")));
16697
+ const privacyPath = join47(config.nativeProjectDirectory, "ios/App/App/PrivacyInfo.xcprivacy");
16698
+ const privacyCurrent = await optionalSource(privacyPath);
16699
+ const privacySource = privacyManifestSource(privacyCurrent, requirements);
16700
+ const [privacyChanged, projectChanged] = await Promise.all([
16701
+ writeIosPrivacyManifest(privacyPath, privacyCurrent, privacySource),
16702
+ configureIosPrivacyProject(config, requirements)
16703
+ ]);
16704
+ return infoChanged || privacyChanged || projectChanged;
16520
16705
  }, configureAndroid2 = async (config, plan) => {
16521
16706
  const path = join47(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
16522
16707
  const source = await readFile12(path, "utf8");
@@ -17706,6 +17891,228 @@ var init_iosConformance = __esm(() => {
17706
17891
  HMR_LINE = new RegExp(String.raw`\[hmr:ios\]\s+([^\n]*?)\s+(applied in|falling back to reload after|failed after)\s+(\d+)ms(?:; server\s+(\d+)ms, client\s+(\d+)ms)?`, "u");
17707
17892
  });
17708
17893
 
17894
+ // src/mobile/nativeTestReport.ts
17895
+ import { mkdir as mkdir13, readFile as readFile18, writeFile as writeFile15 } from "fs/promises";
17896
+ import { join as join51 } from "path";
17897
+ var secretPattern, bearerPattern, coordinatePattern, sanitizeNativeReportText = (value) => value.replace(bearerPattern, "Bearer [REDACTED]").replace(secretPattern, "$1$2[REDACTED]").replace(coordinatePattern, "$1$2[REDACTED]").replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/giu, "$1?[REDACTED]"), markdownCell = (value) => sanitizeNativeReportText(value).replaceAll("|", "\\|").replaceAll(`
17898
+ `, "<br>"), createAbsoluteNativeAutomatedChecks = (run) => {
17899
+ const target = `${run.targetKind} ${run.targetId}`;
17900
+ const evidence = run.screenshot ? `Screenshot: ${run.screenshot}` : undefined;
17901
+ let hmrResult = "NOT_RUN";
17902
+ if (run.hmr)
17903
+ hmrResult = run.hmr.outcome === "failed" ? "FAIL" : "PASS";
17904
+ const routeDetails = run.routes?.length ? ` Routes: ${run.routes.join(", ")}.` : "";
17905
+ return [
17906
+ {
17907
+ details: `Captured host, toolchain, Bun, and AbsoluteJS metadata for ${target}.`,
17908
+ id: "AUTO-SETUP-01",
17909
+ result: "PASS"
17910
+ },
17911
+ {
17912
+ details: run.status === "pass" ? `The app launched and connected to native HMR in ${run.durationMs}ms.${routeDetails}` : `The automated native run failed after ${run.durationMs}ms: ${run.error ?? "No error detail was available."}`,
17913
+ ...evidence ? { evidence } : {},
17914
+ id: "AUTO-DEV-01",
17915
+ result: run.status === "pass" ? "PASS" : "FAIL"
17916
+ },
17917
+ {
17918
+ details: run.hmr ? `Observed native HMR ${run.hmr.outcome} in ${run.hmr.durationMs}ms${run.hmr.serverMs === undefined ? "" : ` (server ${run.hmr.serverMs}ms, client ${run.hmr.clientMs}ms)`}.` : "Correlated edit timing was not requested. Rerun with --wait-for-hmr.",
17919
+ id: "AUTO-HMR-01",
17920
+ result: hmrResult
17921
+ },
17922
+ {
17923
+ details: run.screenshot ? "A target screenshot was captured. Visually review it before sharing this directory." : "No target screenshot was captured.",
17924
+ ...evidence ? { evidence } : {},
17925
+ id: "AUTO-ARTIFACT-01",
17926
+ result: run.screenshot ? "PASS" : "FAIL"
17927
+ }
17928
+ ];
17929
+ }, createAbsoluteNativeTestReport = (options) => ({
17930
+ automatedChecks: options.automatedChecks ?? createAbsoluteNativeAutomatedChecks(options.run),
17931
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
17932
+ manualChecks: options.manualChecks.map(([id, details]) => ({
17933
+ details,
17934
+ id,
17935
+ result: "NOT_RUN"
17936
+ })),
17937
+ metadata: options.metadata,
17938
+ overallResult: options.run.status === "fail" ? "FAIL" : "INCOMPLETE",
17939
+ platform: options.run.platform,
17940
+ reportVersion: 1,
17941
+ run: options.run
17942
+ }), readPackageVersionForNativeReport = async (packageJsonPath) => {
17943
+ const manifest = JSON.parse(await readFile18(packageJsonPath, "utf8"));
17944
+ if (typeof manifest !== "object" || manifest === null)
17945
+ return "unknown";
17946
+ const version2 = Reflect.get(manifest, "version");
17947
+ return typeof version2 === "string" ? version2 : "unknown";
17948
+ }, renderAbsoluteNativeTestReport = (report) => {
17949
+ const table = (checks) => checks.map((check2) => `| ${check2.id} | ${check2.result} | ${markdownCell(check2.details)} | ${markdownCell(check2.evidence ?? "")} |`).join(`
17950
+ `);
17951
+ const metadata = Object.entries(report.metadata).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `- ${key}: ${markdownCell(value)}`).join(`
17952
+ `);
17953
+ return `# AbsoluteJS ${report.platform} test report
17954
+
17955
+ - Overall result: ${report.overallResult}
17956
+ - Generated: ${report.generatedAt}
17957
+ - App bundle ID: ${markdownCell(report.run.appId)}
17958
+ - Target: ${report.run.targetKind} ${markdownCell(report.run.targetId)}
17959
+ - Automated run: ${report.run.status.toUpperCase()} (${report.run.durationMs}ms)
17960
+ ${metadata}
17961
+
17962
+ This report is local and is never uploaded by AbsoluteJS. Before sharing its directory, visually inspect screenshots and logs. Never add passwords, signing material, tokens, cookies, private Sync data, or exact coordinates.
17963
+
17964
+ ## Automated checks
17965
+
17966
+ | Test ID | Result | Observed result / timing | Evidence |
17967
+ | --- | --- | --- | --- |
17968
+ ${table(report.automatedChecks)}
17969
+
17970
+ ## Manual checklist
17971
+
17972
+ Replace each \`NOT_RUN\` with \`PASS\`, \`FAIL\`, or \`SKIPPED\` after completing the platform runbook. A failure must name sanitized evidence and state actual versus expected behavior.
17973
+
17974
+ | Test ID | Result | Observed result / timing | Evidence or failure details |
17975
+ | --- | --- | --- | --- |
17976
+ ${table(report.manualChecks)}
17977
+ `;
17978
+ }, writeAbsoluteNativeTestReport = async (directory, report) => {
17979
+ await mkdir13(directory, { recursive: true });
17980
+ const jsonPath = join51(directory, "report.json");
17981
+ const markdownPath = join51(directory, "report.md");
17982
+ await Promise.all([
17983
+ writeFile15(jsonPath, `${JSON.stringify(report, null, 2)}
17984
+ `),
17985
+ writeFile15(markdownPath, renderAbsoluteNativeTestReport(report))
17986
+ ]);
17987
+ return { directory, jsonPath, markdownPath };
17988
+ };
17989
+ var init_nativeTestReport = __esm(() => {
17990
+ secretPattern = /(authorization|access[_ -]?token|refresh[_ -]?token|socket[_ -]?ticket|password|cookie)(\s*[=:]\s*)([^\s,;]+)/giu;
17991
+ bearerPattern = /bearer\s+[^\s,;]+/giu;
17992
+ coordinatePattern = /\b(latitude|longitude|lat|lng)(\s*[=:]\s*)-?\d+(?:\.\d+)?/giu;
17993
+ });
17994
+
17995
+ // src/mobile/iosTestReport.ts
17996
+ var MANUAL_CHECKS, readPackageVersionForIosReport, sanitizeIosReportText, writeAbsoluteIosPartnerReport, createAbsoluteIosPartnerReport = (options) => {
17997
+ const { udid, ...run } = options.run;
17998
+ return createAbsoluteNativeTestReport({
17999
+ ...options.generatedAt ? { generatedAt: options.generatedAt } : {},
18000
+ manualChecks: MANUAL_CHECKS,
18001
+ metadata: {
18002
+ absolutejsVersion: options.absolutejsVersion,
18003
+ bunVersion: options.bunVersion,
18004
+ macosVersion: options.macosVersion,
18005
+ provider: "capacitor",
18006
+ xcodeVersion: options.xcodeVersion
18007
+ },
18008
+ run: {
18009
+ ...run,
18010
+ platform: "ios",
18011
+ targetId: udid,
18012
+ targetKind: "simulator"
18013
+ }
18014
+ });
18015
+ };
18016
+ var init_iosTestReport = __esm(() => {
18017
+ init_nativeTestReport();
18018
+ MANUAL_CHECKS = [
18019
+ [
18020
+ "SETUP-01",
18021
+ "Confirm and record all Mac, Xcode, Bun, device, and package versions."
18022
+ ],
18023
+ ["SETUP-02", "Confirm Xcode setup and the required iOS runtime."],
18024
+ ["SETUP-03", "Confirm the runbook package versions are installed."],
18025
+ ["SETUP-04", "Confirm the staging bundle ID and production server origin."],
18026
+ ["SETUP-05", "Confirm generated iOS project signing and Xcode warnings."],
18027
+ ["DEV-01", "Record cold and warm bun dev startup timings."],
18028
+ ["DEV-02", "Complete route traversal, HMR, relaunch, and recovery checks."],
18029
+ ["CAP-01", "Complete automatic device-capability provisioning checks."],
18030
+ ...Array.from({ length: 8 }, (_, index) => [
18031
+ `FILES-${String(index + 1).padStart(2, "0")}`,
18032
+ `Complete provider-neutral Documents runbook check FILES-${String(index + 1).padStart(2, "0")}.`
18033
+ ]),
18034
+ ...Array.from({ length: 14 }, (_, index) => [
18035
+ `LOC-${String(index + 1).padStart(2, "0")}`,
18036
+ `Complete foreground-location runbook check LOC-${String(index + 1).padStart(2, "0")} without recording exact coordinates.`
18037
+ ]),
18038
+ [
18039
+ "AUTH-01",
18040
+ "Complete system-browser sign-in, callback, restore, and sign-out checks."
18041
+ ],
18042
+ [
18043
+ "SYNC-01",
18044
+ "Complete online, offline, reconnect, isolation, and conflict checks."
18045
+ ],
18046
+ ["BGSYNC-01", "Complete physical-device background Sync acceptance."],
18047
+ ["REMOTE-01", "Complete remote-Mac acceptance, or mark SKIPPED."],
18048
+ ["BUILD-01", "Pass release doctor and produce a signed IPA."],
18049
+ [
18050
+ "SHIP-01",
18051
+ "Upload, process, assign, install, and launch the TestFlight build."
18052
+ ],
18053
+ ["UPDATE-01", "Prove upload retry reuse and a subsequent web-only update."],
18054
+ [
18055
+ "REPORT-01",
18056
+ "Review this directory for sensitive content and complete every row."
18057
+ ]
18058
+ ];
18059
+ readPackageVersionForIosReport = readPackageVersionForNativeReport;
18060
+ sanitizeIosReportText = sanitizeNativeReportText;
18061
+ writeAbsoluteIosPartnerReport = writeAbsoluteNativeTestReport;
18062
+ });
18063
+
18064
+ // src/mobile/androidTestReport.ts
18065
+ var MANUAL_CHECKS2, createAbsoluteAndroidTestReport = (options) => {
18066
+ const { serial, ...run } = options.run;
18067
+ return createAbsoluteNativeTestReport({
18068
+ ...options.generatedAt ? { generatedAt: options.generatedAt } : {},
18069
+ manualChecks: MANUAL_CHECKS2,
18070
+ metadata: {
18071
+ absolutejsVersion: options.absolutejsVersion,
18072
+ adbVersion: options.adbVersion,
18073
+ bunVersion: options.bunVersion,
18074
+ host: options.host,
18075
+ provider: "capacitor"
18076
+ },
18077
+ run: {
18078
+ ...run,
18079
+ platform: "android",
18080
+ targetId: serial,
18081
+ targetKind: serial.startsWith("emulator-") ? "emulator" : "device"
18082
+ }
18083
+ });
18084
+ };
18085
+ var init_androidTestReport = __esm(() => {
18086
+ init_nativeTestReport();
18087
+ MANUAL_CHECKS2 = [
18088
+ [
18089
+ "SETUP-01",
18090
+ "Confirm Android SDK, emulator/device, Bun, and package versions."
18091
+ ],
18092
+ ["DEV-01", "Record cold and warm native startup timings."],
18093
+ ["DEV-02", "Complete route traversal, HMR, relaunch, and recovery checks."],
18094
+ ["CAP-01", "Complete automatic device-capability provisioning checks."],
18095
+ [
18096
+ "FILES-01",
18097
+ "Complete provider-neutral file pick, export, open, and cleanup checks."
18098
+ ],
18099
+ [
18100
+ "AUTH-01",
18101
+ "Complete system-browser sign-in, callback, restore, and sign-out checks."
18102
+ ],
18103
+ [
18104
+ "SYNC-01",
18105
+ "Complete online, offline, reconnect, isolation, and conflict checks."
18106
+ ],
18107
+ ["BGSYNC-01", "Complete WorkManager background Sync acceptance."],
18108
+ ["BUILD-01", "Pass release doctor and produce a signed AAB."],
18109
+ [
18110
+ "REPORT-01",
18111
+ "Review this directory for sensitive content and complete every row."
18112
+ ]
18113
+ ];
18114
+ });
18115
+
17709
18116
  // src/mobile/releasePublisher.ts
17710
18117
  import { access as access10 } from "fs/promises";
17711
18118
  import { isAbsolute as isAbsolute8, relative as relative26, resolve as resolve40, sep as sep7 } from "path";
@@ -17804,11 +18211,11 @@ var exports_mobile = {};
17804
18211
  __export(exports_mobile, {
17805
18212
  runMobile: () => runMobile
17806
18213
  });
17807
- import { access as access11, mkdir as mkdir13, readFile as readFile18, writeFile as writeFile15 } from "fs/promises";
17808
- import { join as join51, resolve as resolve41 } from "path";
18214
+ import { access as access11, mkdir as mkdir14, readFile as readFile19, writeFile as writeFile16 } from "fs/promises";
18215
+ import { join as join52, resolve as resolve41 } from "path";
17809
18216
  import { createInterface } from "readline/promises";
17810
18217
  var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), CAPACITOR_PACKAGES, CAPACITOR_PACKAGE_SPECS, CAPACITOR_SYNC_PACKAGE_SPECS, packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@")), directProjectPackages = async (projectRoot) => {
17811
- const manifest = JSON.parse(await readFile18(join51(projectRoot, "package.json"), "utf8"));
18218
+ const manifest = JSON.parse(await readFile19(join52(projectRoot, "package.json"), "utf8"));
17812
18219
  if (!isRecord15(manifest))
17813
18220
  throw new TypeError("Application package.json must contain an object.");
17814
18221
  const names = new Set;
@@ -17821,7 +18228,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
17821
18228
  return names;
17822
18229
  }, resolvedPackageVersion = async (projectRoot, packageName) => {
17823
18230
  try {
17824
- const manifest = JSON.parse(await readFile18(join51(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
18231
+ const manifest = JSON.parse(await readFile19(join52(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
17825
18232
  return isRecord15(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
17826
18233
  } catch {
17827
18234
  return;
@@ -17862,7 +18269,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
17862
18269
  }
17863
18270
  return value;
17864
18271
  }, capacitorExecutable = async (projectRoot) => {
17865
- const executable = join51(projectRoot, "node_modules", ".bin", "cap");
18272
+ const executable = join52(projectRoot, "node_modules", ".bin", "cap");
17866
18273
  try {
17867
18274
  await access11(executable);
17868
18275
  return executable;
@@ -18183,7 +18590,7 @@ Mobile release transport checks failed.`);
18183
18590
  const durationMs = Math.round(performance.now() - startedAt);
18184
18591
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
18185
18592
  console.log(`Artifact: ${release.artifactPath}`);
18186
- console.log(`Metadata: ${join51(release.releaseRoot, "release.json")}`);
18593
+ console.log(`Metadata: ${join52(release.releaseRoot, "release.json")}`);
18187
18594
  return release;
18188
18595
  } finally {
18189
18596
  sendTelemetryEvent("mobile:android-release-build", {
@@ -18286,7 +18693,7 @@ Mobile release transport checks failed.`);
18286
18693
  const durationMs = Math.round(performance.now() - startedAt);
18287
18694
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} iOS IPA ${release.metadata.marketingVersion}${release.metadata.buildNumber ? ` (${release.metadata.buildNumber})` : ""} in ${getDurationString(durationMs)}.`);
18288
18695
  console.log(`Artifact: ${release.artifactPath}`);
18289
- console.log(`Metadata: ${join51(release.releaseRoot, "release.json")}`);
18696
+ console.log(`Metadata: ${join52(release.releaseRoot, "release.json")}`);
18290
18697
  return release;
18291
18698
  } finally {
18292
18699
  sendTelemetryEvent("mobile:ios-release-build", {
@@ -18393,7 +18800,7 @@ Mobile release transport checks failed.`);
18393
18800
  checks.push({
18394
18801
  id: "sync.storage-schema",
18395
18802
  label: `Offline schema ${schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ")}`,
18396
- path: join51(projectRoot, "package.json"),
18803
+ path: join52(projectRoot, "package.json"),
18397
18804
  platform: "host",
18398
18805
  status: "pass"
18399
18806
  });
@@ -18401,7 +18808,7 @@ Mobile release transport checks failed.`);
18401
18808
  checks.push({
18402
18809
  id: "sync.storage-schema",
18403
18810
  label: "Offline schema metadata is invalid",
18404
- path: join51(projectRoot, "package.json"),
18811
+ path: join52(projectRoot, "package.json"),
18405
18812
  platform: "host",
18406
18813
  remediation: error instanceof Error ? error.message : String(error),
18407
18814
  status: "fail"
@@ -18554,12 +18961,12 @@ Emulator setup verification:`);
18554
18961
  timeoutMs
18555
18962
  });
18556
18963
  }, writeAndroidFailureArtifacts = async (options) => {
18557
- await mkdir13(options.artifactRoot, { recursive: true });
18558
- const screenshot = options.session ? await options.session.screenshot(join51(options.artifactRoot, "android-failure.png")).catch(() => {
18964
+ await mkdir14(options.artifactRoot, { recursive: true });
18965
+ const screenshot = options.session ? await options.session.screenshot(join52(options.artifactRoot, "android-failure.png")).catch(() => {
18559
18966
  return;
18560
18967
  }) : undefined;
18561
- const diagnosticPath = join51(options.artifactRoot, "android-failure.json");
18562
- await writeFile15(diagnosticPath, `${JSON.stringify({
18968
+ const diagnosticPath = join52(options.artifactRoot, "android-failure.json");
18969
+ await writeFile16(diagnosticPath, `${JSON.stringify({
18563
18970
  diagnostics: options.session?.diagnostics ?? [],
18564
18971
  error: options.error instanceof Error ? options.error.message : String(options.error),
18565
18972
  platform: "android",
@@ -18582,7 +18989,8 @@ Emulator setup verification:`);
18582
18989
  const routes = valuesAfter(args, "--route");
18583
18990
  if (routes.length === 0)
18584
18991
  routes.push(mobile.entry);
18585
- const artifactRoot = safeArtifactRoot(projectRoot, valueAfter(args, "--artifacts"));
18992
+ const reportRoot = nativeReportRoot(args, projectRoot, "android");
18993
+ const artifactRoot = reportRoot ?? safeArtifactRoot(projectRoot, valueAfter(args, "--artifacts"));
18586
18994
  const startedAt = performance.now();
18587
18995
  let session;
18588
18996
  try {
@@ -18621,6 +19029,30 @@ Emulator setup verification:`);
18621
19029
  console.log(JSON.stringify(report, null, 2));
18622
19030
  else
18623
19031
  printAndroidTestReport(report);
19032
+ const screenshot = reportRoot ? await session.screenshot(join52(artifactRoot, "android-emulator.png")) : undefined;
19033
+ await writeRequestedAndroidReport({
19034
+ adb,
19035
+ args,
19036
+ projectRoot,
19037
+ run: {
19038
+ appId: mobile.appId,
19039
+ durationMs: report.durationMs,
19040
+ ...hmrApply ? {
19041
+ hmr: {
19042
+ clientMs: hmrApply.clientMs,
19043
+ durationMs: hmrApply.duration,
19044
+ outcome: hmrApply.outcome,
19045
+ serverMs: hmrApply.serverMs
19046
+ }
19047
+ } : {},
19048
+ hmrConnected: true,
19049
+ port,
19050
+ routes: checks.map(({ route }) => route),
19051
+ ...screenshot ? { screenshot } : {},
19052
+ serial,
19053
+ status: "pass"
19054
+ }
19055
+ });
18624
19056
  return report;
18625
19057
  } catch (error) {
18626
19058
  const durationMs = Math.round(performance.now() - startedAt);
@@ -18639,6 +19071,22 @@ Emulator setup verification:`);
18639
19071
  serial,
18640
19072
  session
18641
19073
  });
19074
+ await writeRequestedAndroidReport({
19075
+ adb,
19076
+ args,
19077
+ projectRoot,
19078
+ run: {
19079
+ appId: mobile.appId,
19080
+ durationMs,
19081
+ error: sanitizeNativeReportText(error instanceof Error ? error.message : String(error)),
19082
+ hmrConnected: false,
19083
+ port,
19084
+ routes,
19085
+ ...screenshot ? { screenshot } : {},
19086
+ serial,
19087
+ status: "fail"
19088
+ }
19089
+ });
18642
19090
  throw new Error(`${error instanceof Error ? error.message : String(error)} Failure diagnostics: ${diagnosticPath}${screenshot ? `; screenshot: ${screenshot}` : ""}`, { cause: error });
18643
19091
  } finally {
18644
19092
  await session?.close();
@@ -18729,14 +19177,14 @@ Emulator setup verification:`);
18729
19177
  if (!selected)
18730
19178
  throw new TypeError("No ready AbsoluteJS iOS simulator was found. Start `bun dev` and wait for the iOS target to become ready.");
18731
19179
  return selected;
18732
- }, requireCapturedIosCommand = (command, label) => {
19180
+ }, requireCapturedCommand = (command, label) => {
18733
19181
  const result = captureCommand4(command);
18734
19182
  if (result.exitCode !== 0)
18735
19183
  throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
18736
19184
  return result;
18737
19185
  }, writeIosFailureArtifacts = async (options) => {
18738
- await mkdir13(options.artifactRoot, { recursive: true });
18739
- const screenshot = join51(options.artifactRoot, "ios-failure.png");
19186
+ await mkdir14(options.artifactRoot, { recursive: true });
19187
+ const screenshot = join52(options.artifactRoot, "ios-failure.png");
18740
19188
  const screenshotResult = captureCommand4([
18741
19189
  options.xcrun,
18742
19190
  "simctl",
@@ -18745,8 +19193,8 @@ Emulator setup verification:`);
18745
19193
  "screenshot",
18746
19194
  screenshot
18747
19195
  ]);
18748
- const diagnosticPath = join51(options.artifactRoot, "ios-failure.json");
18749
- await writeFile15(diagnosticPath, `${JSON.stringify({
19196
+ const diagnosticPath = join52(options.artifactRoot, "ios-failure.json");
19197
+ await writeFile16(diagnosticPath, `${JSON.stringify({
18750
19198
  appId: options.appId,
18751
19199
  error: options.error instanceof Error ? options.error.message : String(options.error),
18752
19200
  platform: "ios",
@@ -18760,6 +19208,64 @@ Emulator setup verification:`);
18760
19208
  diagnosticPath,
18761
19209
  screenshot: screenshotResult.exitCode === 0 ? screenshot : undefined
18762
19210
  };
19211
+ }, nativeReportRoot = (args, projectRoot, platform6) => {
19212
+ const index = args.indexOf("--report");
19213
+ if (index === NOT_FOUND3)
19214
+ return;
19215
+ const candidate = args[index + 1];
19216
+ const explicit = candidate?.startsWith("--") ? undefined : candidate;
19217
+ const timestamp = new Date().toISOString().replaceAll(":", "-");
19218
+ return safeArtifactRoot(projectRoot, explicit ?? `.absolutejs/mobile/test-reports/${platform6}-${timestamp}`);
19219
+ }, absolutejsVersionForReport = async () => {
19220
+ let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
19221
+ const versions = await Promise.all([
19222
+ resolve41(import.meta.dir, "..", "..", "package.json"),
19223
+ resolve41(import.meta.dir, "..", "..", "..", "package.json")
19224
+ ].map((candidate) => readPackageVersionForIosReport(candidate).catch(() => "unknown")));
19225
+ for (const version2 of versions) {
19226
+ if (version2 === "unknown")
19227
+ continue;
19228
+ absolutejsVersion = version2;
19229
+ break;
19230
+ }
19231
+ return absolutejsVersion;
19232
+ }, writeRequestedAndroidReport = async (options) => {
19233
+ const reportRoot = nativeReportRoot(options.args, options.projectRoot, "android");
19234
+ if (!reportRoot)
19235
+ return;
19236
+ const adbVersion = requireCapturedCommand([options.adb, "version"], "ADB version inspection").stdout.trim();
19237
+ const report = createAbsoluteAndroidTestReport({
19238
+ absolutejsVersion: await absolutejsVersionForReport(),
19239
+ adbVersion,
19240
+ bunVersion: Bun.version,
19241
+ host: `${process.platform}-${process.arch}`,
19242
+ run: options.run
19243
+ });
19244
+ const paths = await writeAbsoluteNativeTestReport(reportRoot, report);
19245
+ const print = options.args.includes("--json") ? console.error : console.log;
19246
+ print(`Android test report: ${paths.markdownPath}`);
19247
+ print(`Return this report directory: ${paths.directory}`);
19248
+ return paths;
19249
+ }, iosReportMetadata = async (xcrun) => {
19250
+ const xcodebuildPath = requireCapturedCommand([xcrun, "--find", "xcodebuild"], "Xcode version inspection").stdout.trim();
19251
+ const xcodeVersion = requireCapturedCommand([xcodebuildPath, "-version"], "Xcode version inspection").stdout.trim();
19252
+ const macosVersion = requireCapturedCommand(["/usr/bin/sw_vers", "-productVersion"], "macOS version inspection").stdout.trim();
19253
+ return {
19254
+ absolutejsVersion: await absolutejsVersionForReport(),
19255
+ bunVersion: Bun.version,
19256
+ macosVersion,
19257
+ xcodeVersion
19258
+ };
19259
+ }, writeRequestedIosReport = async (options) => {
19260
+ const reportRoot = nativeReportRoot(options.args, options.projectRoot, "ios");
19261
+ if (!reportRoot)
19262
+ return;
19263
+ const metadata = await iosReportMetadata(options.xcrun);
19264
+ const paths = await writeAbsoluteIosPartnerReport(reportRoot, createAbsoluteIosPartnerReport({ ...metadata, run: options.run }));
19265
+ const print = options.args.includes("--json") ? console.error : console.log;
19266
+ print(`iOS partner report: ${paths.markdownPath}`);
19267
+ print(`Return this report directory: ${paths.directory}`);
19268
+ return paths;
18763
19269
  }, testIos = async (args) => {
18764
19270
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
18765
19271
  const { https, instance, port } = requireIosTestContext(args, projectRoot);
@@ -18770,10 +19276,11 @@ Emulator setup verification:`);
18770
19276
  const timeoutMs = androidTestTimeout(args);
18771
19277
  const xcrun = await requireIosXcrun();
18772
19278
  const simulator = selectIosSimulator(xcrun, valueAfter(args, "--udid") ?? valueAfter(args, "--serial"));
18773
- const artifactRoot = safeArtifactRoot(projectRoot, valueAfter(args, "--artifacts"));
19279
+ const reportRoot = nativeReportRoot(args, projectRoot, "ios");
19280
+ const artifactRoot = reportRoot ?? safeArtifactRoot(projectRoot, valueAfter(args, "--artifacts"));
18774
19281
  const startedAt = performance.now();
18775
19282
  try {
18776
- requireCapturedIosCommand([
19283
+ requireCapturedCommand([
18777
19284
  xcrun,
18778
19285
  "simctl",
18779
19286
  "get_app_container",
@@ -18781,7 +19288,7 @@ Emulator setup verification:`);
18781
19288
  mobile.appId,
18782
19289
  "app"
18783
19290
  ], "iOS installed-app inspection");
18784
- requireCapturedIosCommand([
19291
+ requireCapturedCommand([
18785
19292
  xcrun,
18786
19293
  "simctl",
18787
19294
  "launch",
@@ -18790,9 +19297,9 @@ Emulator setup verification:`);
18790
19297
  mobile.appId
18791
19298
  ], "iOS app launch");
18792
19299
  await waitForIosHmrClient({ https, port, timeoutMs });
18793
- await mkdir13(artifactRoot, { recursive: true });
18794
- const screenshot = join51(artifactRoot, "ios-simulator.png");
18795
- requireCapturedIosCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
19300
+ await mkdir14(artifactRoot, { recursive: true });
19301
+ const screenshot = join52(artifactRoot, "ios-simulator.png");
19302
+ requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
18796
19303
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
18797
19304
  const report = {
18798
19305
  appId: mobile.appId,
@@ -18814,6 +19321,28 @@ Emulator setup verification:`);
18814
19321
  waitedForHmr: args.includes("--wait-for-hmr")
18815
19322
  });
18816
19323
  printIosTestReport(report, args.includes("--json"));
19324
+ await writeRequestedIosReport({
19325
+ args,
19326
+ projectRoot,
19327
+ run: {
19328
+ appId: report.appId,
19329
+ durationMs: report.durationMs,
19330
+ ...report.hmrApply ? {
19331
+ hmr: {
19332
+ ...report.hmrApply.clientMs === undefined ? {} : { clientMs: report.hmrApply.clientMs },
19333
+ durationMs: report.hmrApply.duration,
19334
+ outcome: report.hmrApply.outcome,
19335
+ ...report.hmrApply.serverMs === undefined ? {} : { serverMs: report.hmrApply.serverMs }
19336
+ }
19337
+ } : {},
19338
+ hmrConnected: report.hmrConnected,
19339
+ port: report.port,
19340
+ screenshot: report.screenshot,
19341
+ status: report.status,
19342
+ udid: report.udid
19343
+ },
19344
+ xcrun
19345
+ });
18817
19346
  return report;
18818
19347
  } catch (error) {
18819
19348
  const durationMs = Math.round(performance.now() - startedAt);
@@ -18832,6 +19361,21 @@ Emulator setup verification:`);
18832
19361
  udid: simulator.udid,
18833
19362
  xcrun
18834
19363
  });
19364
+ await writeRequestedIosReport({
19365
+ args,
19366
+ projectRoot,
19367
+ run: {
19368
+ appId: mobile.appId,
19369
+ durationMs,
19370
+ error: sanitizeIosReportText(error instanceof Error ? error.message : String(error)),
19371
+ hmrConnected: false,
19372
+ port,
19373
+ ...screenshot ? { screenshot } : {},
19374
+ status: "fail",
19375
+ udid: simulator.udid
19376
+ },
19377
+ xcrun
19378
+ });
18835
19379
  throw new Error(`${error instanceof Error ? error.message : String(error)} Failure diagnostics: ${diagnosticPath}${screenshot ? `; screenshot: ${screenshot}` : ""}`, { cause: error });
18836
19380
  }
18837
19381
  }, runMobile = async (args) => {
@@ -18888,7 +19432,7 @@ Emulator setup verification:`);
18888
19432
  await publishIos(args.slice(2));
18889
19433
  return;
18890
19434
  }
18891
- throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [--json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | associations [--outdir dir] [--verify] | doctor [ios|android|release] [--remote name] [--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] | test ios [--wait-for-hmr] [--timeout ms] [--port n] [--udid id] [--artifacts dir] [--json]> [--config path]");
19435
+ throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [--json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | associations [--outdir dir] [--verify] | doctor [ios|android|release] [--remote name] [--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] [--report [dir]] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json] | test ios [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--udid id] [--artifacts dir] [--json]> [--config path]");
18892
19436
  };
18893
19437
  var init_mobile = __esm(() => {
18894
19438
  init_dependencies();
@@ -18910,6 +19454,9 @@ var init_mobile = __esm(() => {
18910
19454
  init_iosRelease();
18911
19455
  init_iosSimulatorController();
18912
19456
  init_iosConformance();
19457
+ init_iosTestReport();
19458
+ init_androidTestReport();
19459
+ init_nativeTestReport();
18913
19460
  init_releasePublisher();
18914
19461
  init_start();
18915
19462
  init_utils();
@@ -18939,8 +19486,8 @@ var init_mobile = __esm(() => {
18939
19486
  "@capacitor/cli@8.5.0",
18940
19487
  "@capacitor/android@8.5.0",
18941
19488
  "@capacitor/ios@8.5.0",
18942
- "@absolutejs/devices@0.3.0",
18943
- "@absolutejs/devices-capacitor@0.4.0"
19489
+ "@absolutejs/devices@0.4.0",
19490
+ "@absolutejs/devices-capacitor@0.5.0"
18944
19491
  ];
18945
19492
  CAPACITOR_SYNC_PACKAGE_SPECS = [
18946
19493
  "@absolutejs/sync-capacitor@0.9.1",
@@ -18953,9 +19500,9 @@ var exports_typecheck = {};
18953
19500
  __export(exports_typecheck, {
18954
19501
  typecheck: () => typecheck
18955
19502
  });
18956
- import { resolve as resolve42, join as join52 } from "path";
19503
+ import { resolve as resolve42, join as join53 } from "path";
18957
19504
  import { existsSync as existsSync42, readFileSync as readFileSync40 } from "fs";
18958
- import { mkdir as mkdir14, writeFile as writeFile16 } from "fs/promises";
19505
+ import { mkdir as mkdir15, writeFile as writeFile17 } from "fs/promises";
18959
19506
  var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve42(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
18960
19507
  if (!existsSync42(resolveConfigPath(configPath2))) {
18961
19508
  const defaultService = {};
@@ -19062,8 +19609,8 @@ Found ${errorCount} error${suffix}.`;
19062
19609
  console.error("\x1B[31m\u2717\x1B[0m vue-tsc is required for Vue type checking. Install it: bun add -d vue-tsc");
19063
19610
  process.exit(1);
19064
19611
  }
19065
- const vueTsconfigPath = join52(cacheDir, "tsconfig.vue-check.json");
19066
- await writeFile16(vueTsconfigPath, JSON.stringify({
19612
+ const vueTsconfigPath = join53(cacheDir, "tsconfig.vue-check.json");
19613
+ await writeFile17(vueTsconfigPath, JSON.stringify({
19067
19614
  compilerOptions: {
19068
19615
  rootDir: ".."
19069
19616
  },
@@ -19082,7 +19629,7 @@ Found ${errorCount} error${suffix}.`;
19082
19629
  ...base,
19083
19630
  "--incremental",
19084
19631
  "--tsBuildInfoFile",
19085
- join52(cacheDir, "vue-tsc.tsbuildinfo")
19632
+ join53(cacheDir, "vue-tsc.tsbuildinfo")
19086
19633
  ]);
19087
19634
  if (cached.exitCode === 0 || cached.output.length > 0)
19088
19635
  return cached;
@@ -19093,8 +19640,8 @@ Found ${errorCount} error${suffix}.`;
19093
19640
  console.error("\x1B[31m\u2717\x1B[0m @angular/compiler-cli is required for Angular type checking. Install it: bun add -d @angular/compiler-cli");
19094
19641
  process.exit(1);
19095
19642
  }
19096
- const angularTsconfigPath = join52(cacheDir, "tsconfig.angular-check.json");
19097
- await writeFile16(angularTsconfigPath, JSON.stringify({
19643
+ const angularTsconfigPath = join53(cacheDir, "tsconfig.angular-check.json");
19644
+ await writeFile17(angularTsconfigPath, JSON.stringify({
19098
19645
  angularCompilerOptions: {
19099
19646
  strictTemplates: true
19100
19647
  },
@@ -19113,8 +19660,8 @@ Found ${errorCount} error${suffix}.`;
19113
19660
  console.error("\x1B[31m\u2717\x1B[0m typescript is required for type checking. Install it: bun add -d typescript");
19114
19661
  process.exit(1);
19115
19662
  }
19116
- const tscConfigPath = join52(cacheDir, "tsconfig.typecheck.json");
19117
- return writeFile16(tscConfigPath, JSON.stringify({
19663
+ const tscConfigPath = join53(cacheDir, "tsconfig.typecheck.json");
19664
+ return writeFile17(tscConfigPath, JSON.stringify({
19118
19665
  compilerOptions: {
19119
19666
  rootDir: ".."
19120
19667
  },
@@ -19128,7 +19675,7 @@ Found ${errorCount} error${suffix}.`;
19128
19675
  resolve42(tscConfigPath),
19129
19676
  "--incremental",
19130
19677
  "--tsBuildInfoFile",
19131
- join52(cacheDir, "tsc.tsbuildinfo"),
19678
+ join53(cacheDir, "tsc.tsbuildinfo"),
19132
19679
  "--pretty"
19133
19680
  ]));
19134
19681
  }, buildSvelteCheck = async (cacheDir, svelteDir) => {
@@ -19137,8 +19684,8 @@ Found ${errorCount} error${suffix}.`;
19137
19684
  console.error("\x1B[31m\u2717\x1B[0m svelte-check is required for Svelte type checking. Install it: bun add -d svelte-check");
19138
19685
  process.exit(1);
19139
19686
  }
19140
- const svelteTsconfigPath = join52(cacheDir, "tsconfig.svelte-check.json");
19141
- await writeFile16(svelteTsconfigPath, JSON.stringify({
19687
+ const svelteTsconfigPath = join53(cacheDir, "tsconfig.svelte-check.json");
19688
+ await writeFile17(svelteTsconfigPath, JSON.stringify({
19142
19689
  extends: resolve42("tsconfig.json"),
19143
19690
  files: ABSOLUTE_TYPECHECK_FILES,
19144
19691
  include: [`../${svelteDir}/**/*`]
@@ -19167,7 +19714,7 @@ Found ${errorCount} error${suffix}.`;
19167
19714
  ...new Set(targets.map((config) => config.angularDirectory).filter((dir) => typeof dir === "string" && dir.length > 0))
19168
19715
  ];
19169
19716
  const cacheDir = ".absolutejs";
19170
- await mkdir14(cacheDir, { recursive: true });
19717
+ await mkdir15(cacheDir, { recursive: true });
19171
19718
  const checks = [];
19172
19719
  checks.push(hasVue ? buildVueTscCheck(cacheDir) : buildTscCheck(cacheDir));
19173
19720
  for (const svelteDir of hasSvelte ? svelteDirs : []) {