peachsim 0.1.1620 → 0.1.1635

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.
Files changed (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -7,6 +7,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
8
8
  var __getProtoOf = Object.getPrototypeOf;
9
9
  var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __esm = (fn, res) => function __init() {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ };
10
13
  var __export = (target, all) => {
11
14
  for (var name in all)
12
15
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -29,6 +32,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
32
  ));
30
33
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
34
 
35
+ // src/process-group.ts
36
+ var init_process_group = __esm({
37
+ "src/process-group.ts"() {
38
+ "use strict";
39
+ }
40
+ });
41
+
32
42
  // src/vite-plugin-one.ts
33
43
  var vite_plugin_one_exports = {};
34
44
  __export(vite_plugin_one_exports, {
@@ -657,6 +667,7 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
657
667
  var import_fs = __toESM(require("fs"), 1);
658
668
  var import_path = __toESM(require("path"), 1);
659
669
  var import_lazy_stub_patterns = require("@peach/compat/lazy-stub-patterns");
670
+ var import_native_package_names = require("@peach/compat/native-package-names");
660
671
  var import_stub_manifest = require("@peach/compat/stub-manifest");
661
672
 
662
673
  // ../peach-engine/src/stub-pattern-matcher.ts
@@ -677,7 +688,7 @@ function extractPackageFromPath(modulePath) {
677
688
  }
678
689
 
679
690
  // src/installed-package-versions.ts
680
- function readInstalledVersion(projectRoot, name) {
691
+ function readInstalledPackage(projectRoot, name) {
681
692
  let dir = projectRoot;
682
693
  for (; ; ) {
683
694
  let manifest;
@@ -694,8 +705,7 @@ function readInstalledVersion(projectRoot, name) {
694
705
  dir = parent;
695
706
  continue;
696
707
  }
697
- const version = typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "version") : void 0;
698
- return typeof version === "string" && version.length > 0 ? version : null;
708
+ return typeof manifest === "object" && manifest !== null ? { root: import_path.default.join(dir, "node_modules", name), manifest } : null;
699
709
  }
700
710
  }
701
711
  function readInstalledPackageVersions(projectRoot, modulePaths) {
@@ -706,19 +716,59 @@ function readInstalledPackageVersions(projectRoot, modulePaths) {
706
716
  }
707
717
  const versions = {};
708
718
  for (const name of [...names].sort()) {
709
- const version = readInstalledVersion(projectRoot, name);
710
- if (version) versions[name] = version;
719
+ const installed = readInstalledPackage(projectRoot, name);
720
+ const version = installed && Reflect.get(installed.manifest, "version");
721
+ if (typeof version === "string" && version.length > 0) versions[name] = version;
711
722
  }
712
723
  return versions;
713
724
  }
714
725
  function readInstalledNativePackageVersions(projectRoot) {
726
+ const appRoot = import_fs.default.realpathSync(projectRoot);
727
+ const nativeNames = /* @__PURE__ */ new Set();
728
+ for (const specifier of [
729
+ "react-native",
730
+ ...import_native_package_names.NATIVE_RUNTIME_PACKAGE_NAMES,
731
+ ...import_lazy_stub_patterns.LAZY_STUB_PATTERNS,
732
+ ...Object.values(import_stub_manifest.COMPAT_STUBS).map((entry) => entry.specifier)
733
+ ]) {
734
+ const name = extractPackageFromPath(`node_modules/${specifier}`);
735
+ if (name) nativeNames.add(name);
736
+ }
737
+ const manifest = JSON.parse(
738
+ import_fs.default.readFileSync(import_path.default.join(appRoot, "package.json"), "utf8")
739
+ );
740
+ if (typeof manifest !== "object" || manifest === null) {
741
+ throw new Error("app package.json must contain an object");
742
+ }
743
+ const pending = [{ root: appRoot, manifest }];
744
+ const visited = /* @__PURE__ */ new Set();
745
+ const needed = /* @__PURE__ */ new Set();
746
+ for (let next = pending.pop(); next; next = pending.pop()) {
747
+ const root = import_fs.default.realpathSync(next.root);
748
+ if (visited.has(root)) continue;
749
+ visited.add(root);
750
+ const fields = ["dependencies", "optionalDependencies", "peerDependencies"];
751
+ if (root === appRoot) fields.push("devDependencies");
752
+ const peers = Reflect.get(next.manifest, "peerDependenciesMeta");
753
+ for (const field of fields) {
754
+ const dependencies = Reflect.get(next.manifest, field);
755
+ if (typeof dependencies !== "object" || dependencies === null) continue;
756
+ for (const name of Object.keys(dependencies)) {
757
+ if (field === "peerDependencies" && typeof peers === "object" && peers !== null) {
758
+ const peer = Reflect.get(peers, name);
759
+ if (typeof peer === "object" && peer !== null && Reflect.get(peer, "optional"))
760
+ continue;
761
+ }
762
+ const installed = readInstalledPackage(root, name);
763
+ if (!installed) continue;
764
+ if (nativeNames.has(name)) needed.add(name);
765
+ pending.push(installed);
766
+ }
767
+ }
768
+ }
715
769
  return readInstalledPackageVersions(
716
770
  projectRoot,
717
- [
718
- "react-native",
719
- ...import_lazy_stub_patterns.LAZY_STUB_PATTERNS,
720
- ...Object.values(import_stub_manifest.COMPAT_STUBS).map((entry) => entry.specifier)
721
- ].map((specifier) => `node_modules/${specifier}`)
771
+ [...needed].map((specifier) => `node_modules/${specifier}`)
722
772
  );
723
773
  }
724
774
 
@@ -797,6 +847,8 @@ function readPackageFile(root, file, maxBytes) {
797
847
  // src/swift-submit.ts
798
848
  var SWIFT_PACKAGE_PATH = "/__peach/swift-package/";
799
849
  var SWIFT_HMR_PATH = "/__peach/swift-hmr";
850
+ var DEFAULT_SWIFT_COMPILE_BUILD_MS = 10 * 6e4;
851
+ var MAX_SWIFT_COMPILE_BUILD_MS = 60 * 6e4;
800
852
  var MAX_SWIFT_COMPILE_SOURCE_BYTES = 2 * 1024 * 1024;
801
853
  var MAX_SWIFT_COMPILE_RESOURCE_BYTES = 2 * 1024 * 1024;
802
854
  var MAX_SWIFT_COMPILE_RESOURCE_TOTAL_BYTES = 8 * 1024 * 1024;
@@ -971,13 +1023,14 @@ function findBundleFooter(source, prefix) {
971
1023
  if (newline < 0) return -1;
972
1024
  const line = newline + 1;
973
1025
  if (source.startsWith(prefix, line)) return newline;
974
- if (line !== end && !source.startsWith("globalThis.", line) && !source.startsWith("//", line)) {
975
- return -1;
976
- }
1026
+ if (!isFooterBlockLine(source, line, end)) return -1;
977
1027
  end = newline;
978
1028
  }
979
1029
  return -1;
980
1030
  }
1031
+ function isFooterBlockLine(source, line, end) {
1032
+ return line === end || source.startsWith("globalThis.", line) || source.startsWith("//", line);
1033
+ }
981
1034
  function readFooter(source, prefix, suffix, name) {
982
1035
  const start = findBundleFooter(source, prefix);
983
1036
  if (start < 0) return null;
@@ -1034,6 +1087,9 @@ var import_fs2 = __toESM(require("fs"), 1);
1034
1087
  var import_path2 = __toESM(require("path"), 1);
1035
1088
  var import_url = require("url");
1036
1089
 
1090
+ // ../contrast-runtime-delivery/src/index.ts
1091
+ init_process_group();
1092
+
1037
1093
  // ../contrast-runtime-delivery/src/version.ts
1038
1094
  function isValidRuntimeVersion(value) {
1039
1095
  return value.length <= 100 && /^[0-9A-Za-z][0-9A-Za-z._+-]*$/.test(value);
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -119,6 +119,17 @@ function declareParam(inner, fn, index, param) {
119
119
  declare(inner, name, { kind: "param", fn, index });
120
120
  return;
121
121
  }
122
+ if (isNode(param) && param.type === "ArrayPattern") {
123
+ const elements = Reflect.get(param, "elements");
124
+ if (!Array.isArray(elements)) return;
125
+ elements.forEach((element, position) => {
126
+ const name2 = identifierName(element);
127
+ if (name2 !== void 0)
128
+ declare(inner, name2, { kind: "param", fn, index, key: String(position) });
129
+ else declareOpaque(inner, patternNames(element));
130
+ });
131
+ return;
132
+ }
122
133
  if (!isNode(param) || param.type !== "ObjectPattern") {
123
134
  declareOpaque(inner, patternNames(param));
124
135
  return;
@@ -267,6 +278,17 @@ function immediateArgument(fn, index, collection) {
267
278
  return { argument: args[index], scope: immediate.scope };
268
279
  }
269
280
  function propertyValue(object, name) {
281
+ if (object.type === "ArrayExpression") {
282
+ const index = Number(name);
283
+ const elements = Reflect.get(object, "elements");
284
+ if (!Number.isSafeInteger(index) || index < 0 || String(index) !== name)
285
+ return void 0;
286
+ if (!Array.isArray(elements)) return void 0;
287
+ if (elements.slice(0, index + 1).some((element) => isNode(element) && element.type === "SpreadElement"))
288
+ return void 0;
289
+ const value2 = elements[index];
290
+ return isNode(value2) ? value2 : void 0;
291
+ }
270
292
  let value;
271
293
  for (const property of childNodes(Reflect.get(object, "properties"))) {
272
294
  if (property.type === "SpreadElement") return void 0;
@@ -284,7 +306,8 @@ function resolveProperty(owner, name, collection, depth) {
284
306
  }
285
307
  function resolveObject(expression, scope, collection, depth) {
286
308
  if (depth > MAX_RESOLUTION_DEPTH) return void 0;
287
- if (expression.type === "ObjectExpression") return { object: expression, scope };
309
+ if (expression.type === "ObjectExpression" || expression.type === "ArrayExpression")
310
+ return { object: expression, scope };
288
311
  if (expression.type === "SequenceExpression") {
289
312
  const last = childNodes(Reflect.get(expression, "expressions")).at(-1);
290
313
  return last && resolveObject(last, scope, collection, depth + 1);
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/worklet-executable.ts
4
4
  import { parse } from "acorn";
@@ -94,6 +94,17 @@ function declareParam(inner, fn, index, param) {
94
94
  declare(inner, name, { kind: "param", fn, index });
95
95
  return;
96
96
  }
97
+ if (isNode(param) && param.type === "ArrayPattern") {
98
+ const elements = Reflect.get(param, "elements");
99
+ if (!Array.isArray(elements)) return;
100
+ elements.forEach((element, position) => {
101
+ const name2 = identifierName(element);
102
+ if (name2 !== void 0)
103
+ declare(inner, name2, { kind: "param", fn, index, key: String(position) });
104
+ else declareOpaque(inner, patternNames(element));
105
+ });
106
+ return;
107
+ }
97
108
  if (!isNode(param) || param.type !== "ObjectPattern") {
98
109
  declareOpaque(inner, patternNames(param));
99
110
  return;
@@ -242,6 +253,17 @@ function immediateArgument(fn, index, collection) {
242
253
  return { argument: args[index], scope: immediate.scope };
243
254
  }
244
255
  function propertyValue(object, name) {
256
+ if (object.type === "ArrayExpression") {
257
+ const index = Number(name);
258
+ const elements = Reflect.get(object, "elements");
259
+ if (!Number.isSafeInteger(index) || index < 0 || String(index) !== name)
260
+ return void 0;
261
+ if (!Array.isArray(elements)) return void 0;
262
+ if (elements.slice(0, index + 1).some((element) => isNode(element) && element.type === "SpreadElement"))
263
+ return void 0;
264
+ const value2 = elements[index];
265
+ return isNode(value2) ? value2 : void 0;
266
+ }
245
267
  let value;
246
268
  for (const property of childNodes(Reflect.get(object, "properties"))) {
247
269
  if (property.type === "SpreadElement") return void 0;
@@ -259,7 +281,8 @@ function resolveProperty(owner, name, collection, depth) {
259
281
  }
260
282
  function resolveObject(expression, scope, collection, depth) {
261
283
  if (depth > MAX_RESOLUTION_DEPTH) return void 0;
262
- if (expression.type === "ObjectExpression") return { object: expression, scope };
284
+ if (expression.type === "ObjectExpression" || expression.type === "ArrayExpression")
285
+ return { object: expression, scope };
263
286
  if (expression.type === "SequenceExpression") {
264
287
  const last = childNodes(Reflect.get(expression, "expressions")).at(-1);
265
288
  return last && resolveObject(last, scope, collection, depth + 1);
@@ -4,18 +4,21 @@ import androidx.compose.ui.graphics.Brush
4
4
  import androidx.compose.ui.graphics.Color
5
5
  import androidx.compose.ui.graphics.ImageBitmap
6
6
  import androidx.compose.ui.graphics.SolidColor
7
- import androidx.compose.ui.graphics.toComposeImageBitmap
7
+ import androidx.compose.ui.graphics.asComposeImageBitmap
8
8
  import androidx.compose.ui.graphics.vector.ImageVector
9
9
  import kotlin.io.encoding.Base64
10
10
  import kotlin.io.encoding.ExperimentalEncodingApi
11
+ import org.jetbrains.skia.Bitmap as SkiaBitmap
11
12
  import org.jetbrains.skia.BlendMode
13
+ import org.jetbrains.skia.Canvas
14
+ import org.jetbrains.skia.Codec
15
+ import org.jetbrains.skia.Data
12
16
  import org.jetbrains.skia.FilterMipmap
13
17
  import org.jetbrains.skia.FilterMode
14
18
  import org.jetbrains.skia.Image
15
19
  import org.jetbrains.skia.MipmapMode
16
20
  import org.jetbrains.skia.Paint
17
21
  import org.jetbrains.skia.Rect
18
- import org.jetbrains.skia.Surface
19
22
 
20
23
  // one file a drawable resource resolves to under a density (ResTable_config's
21
24
  // value, 0 for an unqualified directory) and a platform version: a vector the
@@ -67,28 +70,32 @@ private fun isBetter(a: PeachDrawable, b: PeachDrawable, requested: Int): Boolea
67
70
  // bitmap drawn for a higher density than the display's is scaled down, its
68
71
  // size rounded, through a linear filtered draw with the source blend; one
69
72
  // drawn for a lower density, or nodpi, keeps its pixels, as from API 28 the
70
- // upscale is left to draw time, which BitmapPainter does not do
73
+ // upscale is left to draw time, which BitmapPainter does not do. as
74
+ // ImageDecoder does, the codec decodes into a bitmap and the scale draws into
75
+ // another
71
76
  @OptIn(ExperimentalEncodingApi::class)
72
77
  internal fun peachDecodeBitmap(drawable: PeachDrawable.Bitmap, targetDensity: Int): ImageBitmap {
73
78
  if (drawable.ninePatch)
74
79
  throw UnsupportedOperationException("peach kotlin: the nine-patch drawable ${drawable.path} is not drawn yet")
75
- val image = Image.makeFromEncoded(Base64.decode(drawable.data))
80
+ val decoded = Codec.makeFromData(Data.makeFromBytes(Base64.decode(drawable.data))).readPixels()
76
81
  val density = if (drawable.density == DENSITY_DEFAULT) DENSITY_MEDIUM else drawable.density
77
- if (drawable.density == DENSITY_NONE || density <= targetDensity) return image.toComposeImageBitmap()
82
+ if (drawable.density == DENSITY_NONE || density <= targetDensity) return decoded.asComposeImageBitmap()
78
83
  val scale = targetDensity.toFloat() / density
79
- val width = maxOf((image.width * scale + 0.5f).toInt(), 1)
80
- val height = maxOf((image.height * scale + 0.5f).toInt(), 1)
81
- val surface = Surface.makeRasterN32Premul(width, height)
82
- surface.canvas.scale(width.toFloat() / image.width, height.toFloat() / image.height)
83
- surface.canvas.drawImageRect(
84
- image,
85
- Rect.makeWH(image.width.toFloat(), image.height.toFloat()),
86
- Rect.makeWH(image.width.toFloat(), image.height.toFloat()),
84
+ val width = maxOf((decoded.width * scale + 0.5f).toInt(), 1)
85
+ val height = maxOf((decoded.height * scale + 0.5f).toInt(), 1)
86
+ val scaled = SkiaBitmap().apply { allocN32Pixels(width, height) }
87
+ val canvas = Canvas(scaled)
88
+ canvas.scale(width.toFloat() / decoded.width, height.toFloat() / decoded.height)
89
+ canvas.drawImageRect(
90
+ Image.makeFromBitmap(decoded),
91
+ Rect.makeWH(decoded.width.toFloat(), decoded.height.toFloat()),
92
+ Rect.makeWH(decoded.width.toFloat(), decoded.height.toFloat()),
87
93
  FilterMipmap(FilterMode.LINEAR, MipmapMode.NONE),
88
94
  Paint().apply { blendMode = BlendMode.SRC },
89
95
  false,
90
96
  )
91
- return surface.makeImageSnapshot().toComposeImageBitmap()
97
+ canvas.close()
98
+ return scaled.asComposeImageBitmap()
92
99
  }
93
100
 
94
101
  // ComplexColorCompat.willDraw: a transparent solid color draws nothing
@@ -0,0 +1,14 @@
1
+ package androidx.annotation
2
+
3
+ // androidx.annotation's DrawableRes, which only its android artifact declares
4
+ @MustBeDocumented
5
+ @Retention(AnnotationRetention.BINARY)
6
+ @Target(
7
+ AnnotationTarget.FUNCTION,
8
+ AnnotationTarget.PROPERTY_GETTER,
9
+ AnnotationTarget.PROPERTY_SETTER,
10
+ AnnotationTarget.VALUE_PARAMETER,
11
+ AnnotationTarget.FIELD,
12
+ AnnotationTarget.LOCAL_VARIABLE,
13
+ )
14
+ annotation class DrawableRes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.1620",
3
+ "version": "0.1.1635",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -371,7 +371,7 @@
371
371
  "rollup": "4.62.2",
372
372
  "systemjs": "6.15.1",
373
373
  "tsconfck": "3.1.6",
374
- "typescript": "7.1.0-dev.20261003.1",
374
+ "typescript": "7.1.0-beta",
375
375
  "vite": "8.3.3",
376
376
  "yaml": "^2.8.3"
377
377
  }
@@ -24,6 +24,8 @@ import {
24
24
  normalizeHostname,
25
25
  } from '../src/backend-origin.ts'
26
26
  import { applyPeachConfigToUrl } from '../src/config.ts'
27
+ import { fetchDevManifest } from '../src/dev-bundle-resolution'
28
+ import { parseExpoManifestBody } from '../src/expo-manifest-body'
27
29
  import {
28
30
  KOTLIN_DISCOVERY_PATH,
29
31
  KOTLIN_GUEST_PATH,
@@ -448,6 +450,36 @@ function httpGet(
448
450
  })
449
451
  }
450
452
 
453
+ async function probeManifest(
454
+ hostname: string,
455
+ port: number,
456
+ timeout: number,
457
+ ): Promise<HttpResult | null> {
458
+ let deadline: ReturnType<typeof setTimeout> | undefined
459
+ try {
460
+ return await Promise.race([
461
+ fetchDevManifest(buildLoopbackProbeBaseUrl('http:', hostname, port)).then(
462
+ async (res) =>
463
+ res
464
+ ? {
465
+ statusCode: res.status,
466
+ body: await res.text(),
467
+ contentType: res.headers.get('content-type') || '',
468
+ hostname,
469
+ }
470
+ : null,
471
+ ),
472
+ new Promise<null>((resolve) => {
473
+ deadline = setTimeout(() => resolve(null), timeout)
474
+ }),
475
+ ])
476
+ } catch {
477
+ return null
478
+ } finally {
479
+ clearTimeout(deadline)
480
+ }
481
+ }
482
+
451
483
  // ── discovery ───────────────────────────────────────────────────────────────
452
484
 
453
485
  export interface ListeningProcess {
@@ -938,7 +970,7 @@ function applyManifest(
938
970
  ): DiscoveredServer {
939
971
  if (!manifestRes) return result
940
972
  try {
941
- const manifest = JSON.parse(manifestRes.body)
973
+ const manifest = parseExpoManifestBody(manifestRes.body)
942
974
  const client = manifest?.extra?.expoClient || manifest?.extra || {}
943
975
  if (client.name) result.projectName = client.name
944
976
  if (typeof client.primaryColor === 'string') result.primaryColor = client.primaryColor
@@ -1089,16 +1121,7 @@ async function fetchManifestEnrichment(
1089
1121
  buildIconProxyUrl?: (externalUrl: string) => string,
1090
1122
  ): Promise<void> {
1091
1123
  const hostname = normalizeHostname(new URL(result.bundleUrl).hostname)
1092
- const manifestRes = await httpGet(
1093
- hostname,
1094
- result.port,
1095
- '/',
1096
- 'GET',
1097
- ENRICH_TIMEOUT_MS,
1098
- {
1099
- 'expo-platform': 'ios',
1100
- },
1101
- )
1124
+ const manifestRes = await probeManifest(hostname, result.port, ENRICH_TIMEOUT_MS)
1102
1125
  if (!manifestRes || manifestRes.statusCode !== 200) return
1103
1126
  // reuse applyManifest's exact field extraction against a throwaway result.
1104
1127
  const parsed = applyManifest(
@@ -1205,9 +1228,7 @@ export async function probePort(
1205
1228
  httpGet(probeHostname, port, '/status'),
1206
1229
  skipManifest
1207
1230
  ? Promise.resolve(null)
1208
- : httpGet(probeHostname, port, '/', 'GET', MANIFEST_TIMEOUT_MS, {
1209
- 'expo-platform': 'ios',
1210
- }),
1231
+ : probeManifest(probeHostname, port, MANIFEST_TIMEOUT_MS),
1211
1232
  skipExpo ? Promise.resolve(null) : httpGet(probeHostname, port, '/_expo/status'),
1212
1233
  httpGet(probeHostname, port, SWIFT_DISCOVERY_PATH),
1213
1234
  httpGet(probeHostname, port, KOTLIN_DISCOVERY_PATH),
@@ -310,9 +310,14 @@ attributes a missing frame to its hop:
310
310
  - `delivery lag` — vsync postMessage receipt latency above the run's
311
311
  best case; jitter here means the shell worker was descheduled or its
312
312
  queue was busy.
313
- - `paint interval` — the end-to-end cadence the user perceives.
314
- - `idle breaks` — gaps >250ms, excluded from all three: demand-gated
315
- quiesce is legitimate, not starvation.
313
+ - `present interval` — intervals between admitted compositor presentations,
314
+ including native frames, while progress remains pending after the preceding
315
+ presentation (`motionPending` in `compositorCadence.frameSeries`). Settled
316
+ admitted frames end a sequence; deferred frames retain the pending gap.
317
+ `paintIntervals` reports the sample count, so zero samples do not prove
318
+ smoothness. Shell work and JavaScript flushes can park while native frames
319
+ continue presenting.
320
+ - `idle breaks` — host gaps >250ms, excluded from host interval statistics.
316
321
 
317
322
  the compositor `compositorHostFrameClock` keeps its historical name and reports
318
323
  display/timer ticks consumed by the compositor worker's resilient clock during
@@ -320,8 +325,9 @@ the acknowledged profile window. `published` is null because renderer-main no
320
325
  longer publishes compositor frames. sparse consumed ticks with low production
321
326
  time identifies worker scheduling starvation. many consumed ticks with sparse
322
327
  paints can be legitimate dirty-gating; correlate the frame series before
323
- treating it as a regression. shell cadence comes from its worker frame and paint
324
- series.
328
+ treating it as a regression. Host and shell delivery statistics come from the
329
+ shell worker; `cadence.paintInterval*` comes from compositor presentation times
330
+ during motion.
325
331
 
326
332
  compare baseline vs contended captures of the same scripted window; also
327
333
  compare **painted-frame counts**, the crudest and most robust signal.
@@ -42,7 +42,8 @@ debugging or `/peach-test` for writing tests.
42
42
  daemon is down, the dev server port is wrong, or the runtime is missing.
43
43
  walk the recovery checklist before reinstalling.
44
44
  - **keep one sim for your whole session.** the first `peach open <port>`
45
- launches Chrome for Testing with an isolated profile. later opens reuse the
45
+ launches the simulator window: the native desktop app on Apple silicon macOS
46
+ and Linux x64, otherwise Chrome for Testing with an isolated profile. later opens reuse the
46
47
  saved sim and navigate it in place. use `peach do reload` when the target
47
48
  has not changed:
48
49
 
@@ -74,27 +75,23 @@ debugging or `/peach-test` for writing tests.
74
75
  saved sim to reuse. verify the development server and Peach runtime, then
75
76
  retry once after fixing the cause.
76
77
 
77
- ## choose the runtime
78
+ ## the simulator window
78
79
 
79
- on Apple silicon macOS and Linux x64, recommend trying the optional native
80
- runtime for faster simulation. it is beta, so the user may run into more issues
81
- than in the browser. keep the current default unless the user chooses native:
80
+ on Apple silicon macOS and Linux x64, `peach open` shows the app in the native
81
+ desktop app, which is faster than the browser. setup installs it, and on a Mac
82
+ the first `peach open` installs it if it is missing. do not pass a driver flag
83
+ for the user's own viewing.
82
84
 
83
- ```sh
84
- peach desktop install
85
- peach open 8081 --driver desktop
86
- ```
87
-
88
- use the app's actual development server port. the Linux desktop app needs a
89
- desktop session; its `.deb` installer uses `sudo apt-get install`.
90
- `peach desktop install --native` on Linux installs the standalone headless host
91
- instead of the desktop app. Intel Mac and Linux arm64 use the browser.
92
- `peach open 8081 --driver playwright --headed` explicitly opens a browser window.
93
- pass the selected driver when opening the app in the steps below; installing
94
- the desktop app alone does not change the saved window preference.
85
+ the Linux desktop app needs a desktop session; its `.deb` installer uses
86
+ `sudo apt-get install`. `peach desktop install --native` on Linux installs the
87
+ standalone headless host instead of the desktop app. Intel Mac and Linux arm64
88
+ use the browser. `peach open 8081 --driver playwright --headed` explicitly opens
89
+ a browser window. a yellow "GPU disabled" item in the browser simulator's menu
90
+ bar means it is drawing without a GPU and will be slow; move the user to the
91
+ desktop app.
95
92
 
96
- the native runtime renders the simulator. the background service below keeps
97
- the bridge ready for either runtime and is a separate optional choice.
93
+ the background service below keeps the bridge ready for either window and is a
94
+ separate optional choice.
98
95
 
99
96
  ## first move
100
97
 
@@ -1,12 +1,38 @@
1
- // true only inside a mac builder slot that already passed resource admission.
2
- // any other CONTRAST_REMOTE_BUILD value, including "linux" and "0", is an
3
- // inherited marker and is not a reason to compile on this host.
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ export type WindowAdmission =
4
+ | { kind: 'heavy' | 'quiet'; cores: number | null; scopes: string[] }
5
+ | { kind: 'external'; name: string; cores: null }
6
+ // do not cache authority: release returns capacity while this process stays live.
7
+ export function windowAdmission(): WindowAdmission | null {
8
+ const result = spawnSync('tm', ['window', 'ancestor', '--json'], {
9
+ encoding: 'utf8',
10
+ stdio: ['ignore', 'pipe', 'inherit'],
11
+ })
12
+ if (result.error) throw result.error
13
+ if (result.status === 1) return null
14
+ if (result.status !== 0) throw new Error(`tm window ancestor exited ${result.status}`)
15
+ const entry = JSON.parse(result.stdout) as {
16
+ kind: string
17
+ name?: string
18
+ cores?: number
19
+ scopes?: string[]
20
+ }
21
+ if (entry.kind === 'external')
22
+ return { kind: 'external', name: entry.name ?? '', cores: null }
23
+ if (entry.kind !== 'heavy' && entry.kind !== 'quiet')
24
+ throw new Error(`tm window ancestor returned kind ${entry.kind}`)
25
+ return {
26
+ kind: entry.kind,
27
+ cores: entry.cores && entry.cores > 0 ? entry.cores : null,
28
+ scopes: entry.scopes ?? [],
29
+ }
30
+ }
31
+
32
+ // a held destination slot and verified native grant authorize local compilation.
4
33
  export function admittedMacBuilderSlot(
5
34
  env: { readonly [key: string]: string | undefined } = process.env,
6
35
  ): boolean {
7
- return (
8
- env.CONTRAST_REMOTE_BUILD === 'mac' &&
9
- env.REMOTE_MAC_SLOT_HELD === '1' &&
10
- env.CONTRAST_HEAVY_ADMITTED === '1'
11
- )
36
+ if (process.platform !== 'darwin' || env.REMOTE_MAC_SLOT_HELD !== '1') return false
37
+ return windowAdmission() !== null
12
38
  }