peachsim 0.1.2228 → 0.1.2231

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 (100) hide show
  1. package/cli/commands/device.ts +1 -1
  2. package/cli/commands/flow.ts +1 -1
  3. package/dist-lib/agent-daemon-client.cjs +1 -1
  4. package/dist-lib/agent-events.cjs +1 -1
  5. package/dist-lib/attached-projects.cjs +1 -1
  6. package/dist-lib/auth/shared-session.cjs +1 -1
  7. package/dist-lib/backend-origin.cjs +1 -1
  8. package/dist-lib/beta.cjs +1 -1
  9. package/dist-lib/beta.mjs +1 -1
  10. package/dist-lib/bridge-constants.cjs +1 -1
  11. package/dist-lib/bridge-contract-input.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.mjs +1 -1
  13. package/dist-lib/bridge-contract.cjs +1 -1
  14. package/dist-lib/bridge-contract.mjs +1 -1
  15. package/dist-lib/bundle-channel.cjs +35 -9
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cloud-contract.cjs +1 -1
  19. package/dist-lib/cloud-contract.mjs +1 -1
  20. package/dist-lib/cloud-test-args.cjs +1 -1
  21. package/dist-lib/cloud-test-args.mjs +1 -1
  22. package/dist-lib/cloud.cjs +1 -1
  23. package/dist-lib/cloud.mjs +1 -1
  24. package/dist-lib/config.cjs +1 -1
  25. package/dist-lib/config.mjs +1 -1
  26. package/dist-lib/detox/index.cjs +5 -2
  27. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  28. package/dist-lib/expo-swift-bundle.cjs +33 -7
  29. package/dist-lib/expo-swift-metadata.cjs +1 -1
  30. package/dist-lib/expo-swift-metadata.mjs +1 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +76 -11
  33. package/dist-lib/host/fetch-proxy-handler.cjs +4 -4
  34. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  36. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  37. package/dist-lib/host/websocket-proxy.cjs +1 -1
  38. package/dist-lib/index.cjs +13 -5
  39. package/dist-lib/jump-to-source-babel.cjs +1 -1
  40. package/dist-lib/jump-to-source-native.cjs +1 -1
  41. package/dist-lib/kotlin-host.cjs +1 -1
  42. package/dist-lib/menu.cjs +16 -14
  43. package/dist-lib/menu.mjs +16 -14
  44. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  46. package/dist-lib/metro-production-bundle.cjs +1 -1
  47. package/dist-lib/metro-production-bundle.mjs +1 -1
  48. package/dist-lib/metro.cjs +7 -5
  49. package/dist-lib/native-app-identity-project.cjs +5 -3
  50. package/dist-lib/native-app-identity-project.mjs +5 -3
  51. package/dist-lib/native-app-identity.cjs +1 -1
  52. package/dist-lib/native-app-identity.mjs +1 -1
  53. package/dist-lib/native-firebase-metadata.cjs +1 -1
  54. package/dist-lib/native-firebase-metadata.mjs +1 -1
  55. package/dist-lib/pdfium-raster.cjs +1 -1
  56. package/dist-lib/pdfium-raster.mjs +1 -1
  57. package/dist-lib/plist.cjs +1 -1
  58. package/dist-lib/plist.mjs +1 -1
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/public-origins-lib-esm.json +25 -24
  62. package/dist-lib/public-origins-lib.json +55 -53
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/reap-browser.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +43 -3
  67. package/dist-lib/sdk.cjs +1 -1
  68. package/dist-lib/sdk.mjs +1 -1
  69. package/dist-lib/skills.cjs +77 -12
  70. package/dist-lib/swift-compile.cjs +1 -1
  71. package/dist-lib/swift-compile.mjs +1 -1
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +1 -1
  75. package/dist-lib/swift-submit.mjs +1 -1
  76. package/dist-lib/swift-toolchain.cjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +32 -6
  79. package/dist-lib/vite.cjs +10 -4
  80. package/dist-lib/worklet-executable.cjs +1 -1
  81. package/dist-lib/worklet-executable.mjs +1 -1
  82. package/dist-types/packages/peach-engine/src/engine/canvaskit-api.d.ts +3 -0
  83. package/dist-types/packages/peach-engine/src/engine/native-layout.d.ts +10 -3
  84. package/dist-types/packages/peach-engine/src/engine/scene-table.d.ts +1 -1
  85. package/dist-types/packages/peach-engine/src/engine/types.d.ts +2 -1
  86. package/install.sh +12 -2
  87. package/kotlin/sdk.tar.gz +0 -0
  88. package/package.json +1 -1
  89. package/runtime-companion.json +1 -1
  90. package/scripts/dev-server-scanner.ts +64 -3
  91. package/src/appPackageContract.ts +1 -1
  92. package/src/conformance-pixel-comparison.ts +12 -7
  93. package/src/menu.ts +17 -99
  94. package/src/menuContract.ts +102 -0
  95. package/src/react-native-coverage/native-typescript-parser.ts +18 -8
  96. package/src/settingsContract.ts +8 -8
  97. package/src/swift-host.ts +27 -2
  98. package/src/vite-plugin-one.ts +2 -0
  99. package/src/vite-plugin.ts +9 -8
  100. package/src/xcode-project.ts +8 -2
@@ -48,6 +48,7 @@ import {
48
48
  parseSwiftHostDiscovery,
49
49
  } from '../src/swiftDiscoveryContract'
50
50
  import { loadOptionalDemoApps, optionalDemoAppsSync } from './optional-demo-registry.ts'
51
+ import type { ConfiguredOneWidget } from '../src/oneWidgetContract'
51
52
  import type { DiscoveredServer } from '../src/serverDiscoveryContract'
52
53
 
53
54
  export type { DiscoveredServer } from '../src/serverDiscoveryContract'
@@ -1187,6 +1188,38 @@ const knownNonPatched = new Set<number>()
1187
1188
  // same invalidation rules as knownNonPatched: cleared on port
1188
1189
  // disappearance, owning-pid change, and __resetScannerCache.
1189
1190
  const knownOne = new Set<number>()
1191
+ const oneNativeApps = new Map<number, { root: string; bundleId: string }>()
1192
+
1193
+ function oneNativeWidgetFromScan(
1194
+ scan: unknown,
1195
+ result: DiscoveredServer,
1196
+ identity: { root: string; bundleId: string },
1197
+ ): ConfiguredOneWidget | null {
1198
+ if (!Array.isArray(scan)) return null
1199
+ for (const entry of scan) {
1200
+ if (!entry || typeof entry !== 'object' || entry.port !== result.port) continue
1201
+ if (typeof entry.bundleUrl !== 'string') continue
1202
+ let advertised: URL
1203
+ try {
1204
+ advertised = new URL(entry.bundleUrl)
1205
+ } catch {
1206
+ continue
1207
+ }
1208
+ if (!isLoopbackHost(advertised.hostname) || advertised.port !== String(result.port))
1209
+ continue
1210
+ const widget = configuredWidgetFromScan(scan, advertised.href)
1211
+ if (
1212
+ widget?.projectRoot === identity.root &&
1213
+ widget.metadata.bundleId === identity.bundleId
1214
+ )
1215
+ return {
1216
+ ...widget,
1217
+ bundleUrl: result.bundleUrl,
1218
+ packageUrl: new URL('/__peach/swift-package/OneWidgets', result.bundleUrl).href,
1219
+ }
1220
+ }
1221
+ return null
1222
+ }
1190
1223
 
1191
1224
  // fire signature probes in waves and pick the best match by precedence:
1192
1225
  // 1. expo manifest (JSON manifest at /)
@@ -1260,6 +1293,25 @@ export async function probePort(
1260
1293
  const manifestParsed = manifestRes ? safeParseManifest(manifestRes.body) : null
1261
1294
  const { launchUrl: manifestLaunchUrl, client: manifestClient } =
1262
1295
  readExpoManifestFields(manifestParsed)
1296
+ const metadata = manifestParsed?.metadata
1297
+ const extra = manifestParsed?.extra
1298
+ const expoGo = extra && typeof extra === 'object' ? Reflect.get(extra, 'expoGo') : null
1299
+ const developer =
1300
+ expoGo && typeof expoGo === 'object' ? Reflect.get(expoGo, 'developer') : null
1301
+ const projectRoot =
1302
+ developer && typeof developer === 'object'
1303
+ ? Reflect.get(developer, 'projectRoot')
1304
+ : null
1305
+ const bundleId = manifestClient.ios?.bundleIdentifier
1306
+ if (
1307
+ metadata &&
1308
+ typeof metadata === 'object' &&
1309
+ Reflect.get(metadata, 'oneNativeApp') === true &&
1310
+ typeof projectRoot === 'string' &&
1311
+ typeof bundleId === 'string'
1312
+ )
1313
+ oneNativeApps.set(port, { root: projectRoot, bundleId })
1314
+ else if (manifestRes) oneNativeApps.delete(port)
1263
1315
  // `manifestRes === null` means the probe got NO answer — it timed out or the
1264
1316
  // connection failed. that is not the same fact as a server that answered and
1265
1317
  // turned out to have no manifest, and only the second one licenses the
@@ -1636,6 +1688,7 @@ export function __resetScannerCache() {
1636
1688
  portCache.clear()
1637
1689
  knownNonPatched.clear()
1638
1690
  knownOne.clear()
1691
+ oneNativeApps.clear()
1639
1692
  enrichmentCache.clear()
1640
1693
  enrichmentInFlight.clear()
1641
1694
  portCacheInvalidatedAt.clear()
@@ -1692,6 +1745,9 @@ export async function scanDevServers(
1692
1745
  for (const p of [...knownOne]) {
1693
1746
  if (!currentPorts.has(p)) knownOne.delete(p)
1694
1747
  }
1748
+ for (const p of [...oneNativeApps.keys()]) {
1749
+ if (!currentPorts.has(p)) oneNativeApps.delete(p)
1750
+ }
1695
1751
  for (const p of [...enrichmentCache.keys()]) {
1696
1752
  if (!currentPorts.has(p)) enrichmentCache.delete(p)
1697
1753
  }
@@ -1726,6 +1782,7 @@ export async function scanDevServers(
1726
1782
  if (cached && cached.pid !== pid) {
1727
1783
  knownNonPatched.delete(port)
1728
1784
  knownOne.delete(port)
1785
+ oneNativeApps.delete(port)
1729
1786
  // a restarted process may be a different app — drop its captured name/icon.
1730
1787
  enrichmentCache.delete(port)
1731
1788
  localIconPathCache.delete(port)
@@ -1765,7 +1822,8 @@ export async function scanDevServers(
1765
1822
  // "no widget".
1766
1823
  await Promise.all(
1767
1824
  results.map(async (result) => {
1768
- if (result.framework !== 'one' && result.framework !== 'vxrn') {
1825
+ const nativeIdentity = oneNativeApps.get(result.port)
1826
+ if (result.framework !== 'one' && result.framework !== 'vxrn' && !nativeIdentity) {
1769
1827
  result.widget = null
1770
1828
  return
1771
1829
  }
@@ -1780,10 +1838,13 @@ export async function scanDevServers(
1780
1838
  result.widget ??= null
1781
1839
  return
1782
1840
  }
1783
- result.widget =
1841
+ const scan =
1784
1842
  scanRes.statusCode === 200 && scanRes.contentType?.includes('json')
1785
- ? configuredWidgetFromScan(safeParseManifest(scanRes.body), result.bundleUrl)
1843
+ ? safeParseManifest(scanRes.body)
1786
1844
  : null
1845
+ result.widget = nativeIdentity
1846
+ ? oneNativeWidgetFromScan(scan, result, nativeIdentity)
1847
+ : configuredWidgetFromScan(scan, result.bundleUrl)
1787
1848
  }),
1788
1849
  )
1789
1850
 
@@ -1,4 +1,4 @@
1
- import { DESKTOP_RENDER_RESOLUTION } from './menu'
1
+ import { DESKTOP_RENDER_RESOLUTION } from './menuContract'
2
2
  import {
3
3
  isSettingKey,
4
4
  isValidSettingValue,
@@ -1,3 +1,4 @@
1
+ import { Buffer } from 'node:buffer'
1
2
  import pixelmatch from 'pixelmatch'
2
3
  import {
3
4
  CONFORMANCE_FLAT_SHADE_FLATNESS,
@@ -123,13 +124,17 @@ export function compareConformancePixels(options: {
123
124
 
124
125
  // identical frames need no shade or neighborhood search, but still use
125
126
  // the same region and rounded-display exclusions for their pixel count.
126
- let identical = true
127
- for (let index = 0; index < options.reference.length; index++) {
128
- if (options.reference[index] !== options.result[index]) {
129
- identical = false
130
- break
131
- }
132
- }
127
+ const identical = Buffer.from(
128
+ options.reference.buffer,
129
+ options.reference.byteOffset,
130
+ options.reference.byteLength,
131
+ ).equals(
132
+ Buffer.from(
133
+ options.result.buffer,
134
+ options.result.byteOffset,
135
+ options.result.byteLength,
136
+ ),
137
+ )
133
138
  if (identical) {
134
139
  options.diff?.fill(0)
135
140
  const columns = Math.ceil(options.width)
package/src/menu.ts CHANGED
@@ -1,107 +1,25 @@
1
+ import {
2
+ type SootSimMenuPlatform,
3
+ type SootSimScaleMode,
4
+ type SootSimAmbientGlowLevel,
5
+ SOOTSIM_AMBIENT_GLOW_LEVELS,
6
+ SOOTSIM_AMBIENT_GLOW_LABELS,
7
+ type SootSimStageBackground,
8
+ SOOTSIM_STAGE_BACKGROUNDS,
9
+ SOOTSIM_STAGE_BACKGROUND_LABELS,
10
+ type SootSimRenderResolution,
11
+ SOOTSIM_RENDER_RESOLUTION_LEVELS,
12
+ SOOTSIM_RENDER_RESOLUTION_LABELS,
13
+ type SootSimScreenEffect,
14
+ SOOTSIM_SCREEN_EFFECTS,
15
+ SOOTSIM_SCREEN_EFFECT_LABELS,
16
+ } from './menuContract'
1
17
  import { rnxPublicBrand } from './public-brand'
2
18
 
3
19
  // public sootsim menu protocol. this file is deliberately pure data and pure
4
20
  // functions so hosts can import it without pulling the engine or canvaskit graph.
5
21
 
6
- export const SOOTSIM_ENGINE_EVENT_NAME = 'sootsim:engine-event'
7
- export const SOOTSIM_SHELL_COMMAND_NAME = 'sootsim:shell-command'
8
-
9
- export type SootSimMenuPlatform = 'browser' | 'electron'
10
- export type SootSimScaleMode = 'physical' | 'point' | 'pixel' | 'fit'
11
-
12
- // ambient glow strength. one setting with three states rather than an on/off
13
- // pair plus a separate strength: every host that carries the preference (the
14
- // menus, the persisted setting, the ?ambientGlow= boot seed, the live
15
- // set-ambient-glow push, electron's shared config) then carries exactly one
16
- // value and cannot end up half-configured.
17
- export type SootSimAmbientGlowLevel = 'off' | 'soft' | 'full'
18
- export const SOOTSIM_AMBIENT_GLOW_LEVELS = ['off', 'soft', 'full'] as const
19
-
20
- export function isSootSimAmbientGlowLevel(
21
- value: unknown,
22
- ): value is SootSimAmbientGlowLevel {
23
- return value === 'off' || value === 'soft' || value === 'full'
24
- }
25
-
26
- export const SOOTSIM_AMBIENT_GLOW_LABELS: Record<SootSimAmbientGlowLevel, string> = {
27
- off: 'Off',
28
- soft: 'Soft',
29
- full: 'Full',
30
- }
31
-
32
- // the stage behind the device: system follows the mac's appearance (white in
33
- // light mode, black in dark), or one pinned color. one value per host, like
34
- // the glow level above.
35
- export type SootSimStageBackground = 'system' | 'light' | 'dark' | 'gray'
36
- export const SOOTSIM_STAGE_BACKGROUNDS = ['system', 'light', 'dark', 'gray'] as const
37
-
38
- export function isSootSimStageBackground(
39
- value: unknown,
40
- ): value is SootSimStageBackground {
41
- return value === 'system' || value === 'light' || value === 'dark' || value === 'gray'
42
- }
43
-
44
- export const SOOTSIM_STAGE_BACKGROUND_LABELS: Record<SootSimStageBackground, string> = {
45
- system: 'System',
46
- light: 'Light',
47
- dark: 'Dark',
48
- gray: 'Gray',
49
- }
50
-
51
- // render resolution: how many pixels peach renders per iphone point. one
52
- // setting with three states, carried by every host exactly like the ambient
53
- // glow level above: the menus, the persisted setting, the ?renderResolution=
54
- // boot seed, the live set-render-resolution push, electron's shared config.
55
- export type SootSimRenderResolution = 'high' | 'medium' | 'low'
56
- export const SOOTSIM_RENDER_RESOLUTION_LEVELS = ['high', 'medium', 'low'] as const
57
-
58
- // the installed desktop app presents the phone scaled to fit its window, so a
59
- // fixed 2 px/pt canvas is browser-resampled and its icons read soft. its
60
- // default is the level that tracks the pixels on screen; browsers and capture
61
- // keep medium, whose 2 px/pt is the deterministic capture contract.
62
- export const DESKTOP_RENDER_RESOLUTION: SootSimRenderResolution = 'low'
63
-
64
- export function isSootSimRenderResolution(
65
- value: unknown,
66
- ): value is SootSimRenderResolution {
67
- return value === 'high' || value === 'medium' || value === 'low'
68
- }
69
-
70
- export const SOOTSIM_RENDER_RESOLUTION_LABELS: Record<SootSimRenderResolution, string> = {
71
- high: 'High',
72
- medium: 'Medium',
73
- low: 'Low',
74
- }
75
-
76
- // screen effect: the real rendering, or one of the presentation looks the
77
- // engine's wireframe store draws. 'wireframe' is the clean structural sketch;
78
- // the others are its screen-space styles that ship as named effects.
79
- export type SootSimScreenEffect = 'none' | 'wireframe' | 'blueprint' | 'panel' | 'green'
80
- export const SOOTSIM_SCREEN_EFFECTS = [
81
- 'none',
82
- 'wireframe',
83
- 'blueprint',
84
- 'panel',
85
- 'green',
86
- ] as const
87
-
88
- export function isSootSimScreenEffect(value: unknown): value is SootSimScreenEffect {
89
- return (
90
- value === 'none' ||
91
- value === 'wireframe' ||
92
- value === 'blueprint' ||
93
- value === 'panel' ||
94
- value === 'green'
95
- )
96
- }
97
-
98
- export const SOOTSIM_SCREEN_EFFECT_LABELS: Record<SootSimScreenEffect, string> = {
99
- none: 'None',
100
- wireframe: 'Wireframe',
101
- blueprint: 'Blueprint',
102
- panel: 'Panel',
103
- green: 'Green',
104
- }
22
+ export * from './menuContract'
105
23
 
106
24
  export type SootSimMenuItemDef =
107
25
  | { type: 'separator' }
@@ -0,0 +1,102 @@
1
+ // the values and event names the menu protocol carries. kept apart from the menu builder so a
2
+ // runtime that only validates a setting or names an event does not carry the menus at boot.
3
+
4
+ export const SOOTSIM_ENGINE_EVENT_NAME = 'sootsim:engine-event'
5
+ export const SOOTSIM_SHELL_COMMAND_NAME = 'sootsim:shell-command'
6
+
7
+ export type SootSimMenuPlatform = 'browser' | 'electron'
8
+ export type SootSimScaleMode = 'physical' | 'point' | 'pixel' | 'fit'
9
+
10
+ // ambient glow strength. one setting with three states rather than an on/off
11
+ // pair plus a separate strength: every host that carries the preference (the
12
+ // menus, the persisted setting, the ?ambientGlow= boot seed, the live
13
+ // set-ambient-glow push, electron's shared config) then carries exactly one
14
+ // value and cannot end up half-configured.
15
+ export type SootSimAmbientGlowLevel = 'off' | 'soft' | 'full'
16
+ export const SOOTSIM_AMBIENT_GLOW_LEVELS = ['off', 'soft', 'full'] as const
17
+
18
+ export function isSootSimAmbientGlowLevel(
19
+ value: unknown,
20
+ ): value is SootSimAmbientGlowLevel {
21
+ return value === 'off' || value === 'soft' || value === 'full'
22
+ }
23
+
24
+ export const SOOTSIM_AMBIENT_GLOW_LABELS: Record<SootSimAmbientGlowLevel, string> = {
25
+ off: 'Off',
26
+ soft: 'Soft',
27
+ full: 'Full',
28
+ }
29
+
30
+ // the stage behind the device: system follows the mac's appearance (white in
31
+ // light mode, black in dark), or one pinned color. one value per host, like
32
+ // the glow level above.
33
+ export type SootSimStageBackground = 'system' | 'light' | 'dark' | 'gray'
34
+ export const SOOTSIM_STAGE_BACKGROUNDS = ['system', 'light', 'dark', 'gray'] as const
35
+
36
+ export function isSootSimStageBackground(
37
+ value: unknown,
38
+ ): value is SootSimStageBackground {
39
+ return value === 'system' || value === 'light' || value === 'dark' || value === 'gray'
40
+ }
41
+
42
+ export const SOOTSIM_STAGE_BACKGROUND_LABELS: Record<SootSimStageBackground, string> = {
43
+ system: 'System',
44
+ light: 'Light',
45
+ dark: 'Dark',
46
+ gray: 'Gray',
47
+ }
48
+
49
+ // render resolution: how many pixels peach renders per iphone point. one
50
+ // setting with three states, carried by every host exactly like the ambient
51
+ // glow level above: the menus, the persisted setting, the ?renderResolution=
52
+ // boot seed, the live set-render-resolution push, electron's shared config.
53
+ export type SootSimRenderResolution = 'high' | 'medium' | 'low'
54
+ export const SOOTSIM_RENDER_RESOLUTION_LEVELS = ['high', 'medium', 'low'] as const
55
+
56
+ // the installed desktop app presents the phone scaled to fit its window, so a
57
+ // fixed 2 px/pt canvas is browser-resampled and its icons read soft. its
58
+ // default is the level that tracks the pixels on screen; browsers and capture
59
+ // keep medium, whose 2 px/pt is the deterministic capture contract.
60
+ export const DESKTOP_RENDER_RESOLUTION: SootSimRenderResolution = 'low'
61
+
62
+ export function isSootSimRenderResolution(
63
+ value: unknown,
64
+ ): value is SootSimRenderResolution {
65
+ return value === 'high' || value === 'medium' || value === 'low'
66
+ }
67
+
68
+ export const SOOTSIM_RENDER_RESOLUTION_LABELS: Record<SootSimRenderResolution, string> = {
69
+ high: 'High',
70
+ medium: 'Medium',
71
+ low: 'Low',
72
+ }
73
+
74
+ // screen effect: the real rendering, or one of the presentation looks the
75
+ // engine's wireframe store draws. 'wireframe' is the clean structural sketch;
76
+ // the others are its screen-space styles that ship as named effects.
77
+ export type SootSimScreenEffect = 'none' | 'wireframe' | 'blueprint' | 'panel' | 'green'
78
+ export const SOOTSIM_SCREEN_EFFECTS = [
79
+ 'none',
80
+ 'wireframe',
81
+ 'blueprint',
82
+ 'panel',
83
+ 'green',
84
+ ] as const
85
+
86
+ export function isSootSimScreenEffect(value: unknown): value is SootSimScreenEffect {
87
+ return (
88
+ value === 'none' ||
89
+ value === 'wireframe' ||
90
+ value === 'blueprint' ||
91
+ value === 'panel' ||
92
+ value === 'green'
93
+ )
94
+ }
95
+
96
+ export const SOOTSIM_SCREEN_EFFECT_LABELS: Record<SootSimScreenEffect, string> = {
97
+ none: 'None',
98
+ wireframe: 'Wireframe',
99
+ blueprint: 'Blueprint',
100
+ panel: 'Panel',
101
+ green: 'Green',
102
+ }
@@ -61,14 +61,24 @@ export async function withTypeScriptSourceParser<T>(
61
61
  if (!project) throw new Error('native TypeScript parser found no program')
62
62
  // the program retains every input for resolution; transfer only the ASTs
63
63
  // the caller analyzes across the native compiler's RPC boundary.
64
- for (const path of requestedSources) {
65
- const virtualPath = sourcePaths.get(path)
66
- if (!virtualPath) throw new Error(`requested parser source is missing: ${path}`)
67
- const sourceFile = await project.program.getSourceFile(virtualPath)
68
- if (!sourceFile)
69
- throw new Error(`native TypeScript parser did not return ${path}`)
70
- parsed.set(path, sourceFile)
71
- projects.set(path, project)
64
+ const requestedPaths = Array.from(requestedSources)
65
+ for (let start = 0; start < requestedPaths.length; start += 64) {
66
+ const sources = await Promise.allSettled(
67
+ requestedPaths.slice(start, start + 64).map(async (path) => {
68
+ const virtualPath = sourcePaths.get(path)
69
+ if (!virtualPath)
70
+ throw new Error(`requested parser source is missing: ${path}`)
71
+ const sourceFile = await project.program.getSourceFile(virtualPath)
72
+ if (!sourceFile)
73
+ throw new Error(`native TypeScript parser did not return ${path}`)
74
+ return { path, sourceFile }
75
+ }),
76
+ )
77
+ for (const source of sources) {
78
+ if (source.status === 'rejected') throw source.reason
79
+ parsed.set(source.value.path, source.value.sourceFile)
80
+ projects.set(source.value.path, project)
81
+ }
72
82
  }
73
83
  return await analyze(parsed, projects)
74
84
  } finally {
@@ -2,14 +2,6 @@ import {
2
2
  DEFAULT_PROFILER_CATEGORIES,
3
3
  formatProfilerCategories,
4
4
  } from '@peach/globals/profiler'
5
- import {
6
- SOOTSIM_AMBIENT_GLOW_LEVELS,
7
- SOOTSIM_RENDER_RESOLUTION_LEVELS,
8
- SOOTSIM_STAGE_BACKGROUNDS,
9
- type SootSimAmbientGlowLevel,
10
- type SootSimRenderResolution,
11
- type SootSimStageBackground,
12
- } from 'peachsim/menu'
13
5
  import {
14
6
  type DeviceModel,
15
7
  type DevicePosture,
@@ -19,6 +11,14 @@ import {
19
11
  DYNAMIC_WINDOW_LIMITS,
20
12
  devices,
21
13
  } from './device-catalog.ts'
14
+ import {
15
+ SOOTSIM_AMBIENT_GLOW_LEVELS,
16
+ SOOTSIM_RENDER_RESOLUTION_LEVELS,
17
+ SOOTSIM_STAGE_BACKGROUNDS,
18
+ type SootSimAmbientGlowLevel,
19
+ type SootSimRenderResolution,
20
+ type SootSimStageBackground,
21
+ } from './menuContract'
22
22
  import { DEFAULT_REACT_NATIVE_CONFIGURATION } from './reactNativeConfigurationContract.ts'
23
23
  import {
24
24
  BACKGROUND_PRESET_NAMES,
package/src/swift-host.ts CHANGED
@@ -132,8 +132,10 @@ import {
132
132
  } from './swiftContract'
133
133
  import {
134
134
  enclosingXcodeWorkspace,
135
+ expandXcodeBuildSetting,
135
136
  readXcodeAppTarget,
136
137
  resolveXcodeSwiftPackages,
138
+ xcodeFileListEntries,
137
139
  type XcodeAppTarget,
138
140
  type XcodeModuleTarget,
139
141
  } from './xcode-project'
@@ -2312,11 +2314,34 @@ export function buildSwiftBundleFiles(
2312
2314
  CONTENTS_FOLDER_PATH: wrapper,
2313
2315
  UNLOCALIZED_RESOURCES_FOLDER_PATH: wrapper,
2314
2316
  INFOPLIST_PATH: `${wrapper}/Info.plist`,
2315
- SCRIPT_INPUT_FILE_COUNT: '0',
2316
- SCRIPT_INPUT_FILE_LIST_COUNT: '0',
2317
+ SCRIPT_INPUT_FILE_COUNT: String(phase.inputPaths.length),
2318
+ SCRIPT_INPUT_FILE_LIST_COUNT: String(phase.inputFileListPaths.length),
2317
2319
  SCRIPT_OUTPUT_FILE_COUNT: '0',
2318
2320
  SCRIPT_OUTPUT_FILE_LIST_COUNT: '0',
2319
2321
  }
2322
+ const inputPath = (raw: string) => {
2323
+ const file = path.resolve(
2324
+ phase.directory,
2325
+ expandXcodeBuildSetting(raw, (key) => env[key] ?? ''),
2326
+ )
2327
+ if (!fs.existsSync(file))
2328
+ throw new Error(
2329
+ `${xcode.app.name}: Run Script "${phase.name}" missing input ${file}`,
2330
+ )
2331
+ if (!file.startsWith(`${dir}${path.sep}`))
2332
+ watched.push(path.relative(packagePath, file).split(path.sep).join('/'))
2333
+ return file
2334
+ }
2335
+ phase.inputPaths.forEach((raw, index) => {
2336
+ env[`SCRIPT_INPUT_FILE_${index}`] = inputPath(raw)
2337
+ })
2338
+ phase.inputFileListPaths.forEach((raw, index) => {
2339
+ const source = inputPath(raw)
2340
+ const entries = xcodeFileListEntries(source, inputPath)
2341
+ const expanded = path.join(dir, `Input-${index}.xcfilelist`)
2342
+ fs.writeFileSync(expanded, `${entries.join('\n')}\n`)
2343
+ env[`SCRIPT_INPUT_FILE_LIST_${index}`] = expanded
2344
+ })
2320
2345
  try {
2321
2346
  const [shell, ...args] = phase.command
2322
2347
  execFileSync(shell, args, {
@@ -46,6 +46,7 @@ import {
46
46
  } from './runtime-assets'
47
47
  import { ensureRuntimePackFor } from './runtime-packs'
48
48
  import { SWIFT_HMR_PATH, SWIFT_PACKAGE_PATH } from './swift-submit'
49
+ import { privateSwiftDistributionRoot } from './swift-toolchain'
49
50
  import { swiftViteLifetime } from './swift-vite-lifetime'
50
51
  import type { OneWidgetPackage } from './oneWidgetContract'
51
52
  import type { PeachPluginOptions } from './vite'
@@ -235,6 +236,7 @@ export function peachPlugin(options: PeachPluginOptions = {}): Plugin {
235
236
  // npm install has none (null), and its packages compile on Contrast.
236
237
  let swiftModuleRoot: string | null | undefined
237
238
  const moduleRootFor = (): string | undefined => {
239
+ if (privateSwiftDistributionRoot()) return undefined
238
240
  if (swiftModuleRoot === undefined) {
239
241
  // src/ or dist-lib/ → .. is peachsim; the module sits beside it
240
242
  const moduleRoot = path.resolve(
@@ -2542,23 +2542,23 @@ function externalAppResolvePlugin(
2542
2542
  // convert require() calls and handle JSX in .js files from external app sources
2543
2543
  function externalAppTransformPlugin(isAppSource: (p: string) => boolean): Plugin {
2544
2544
  const defines: Record<string, string> = {}
2545
+ let isBuild = false
2545
2546
  return {
2546
2547
  name: 'sootsim-external-app-transform',
2547
2548
  enforce: 'pre',
2548
2549
  configResolved(config) {
2550
+ isBuild = config.command === 'build'
2549
2551
  for (const [name, value] of Object.entries(config.define ?? {}))
2550
2552
  defines[name] = typeof value === 'string' ? value : JSON.stringify(value)
2551
2553
  },
2552
2554
  transform: {
2553
2555
  filter: { code: /require\(|</ },
2554
2556
  async handler(code, id) {
2555
- // expo's winter polyfills use Metro-local requires in lazy getters.
2556
- // their module imports must never reach the host's native-package registry.
2557
- if (!isAppSource(id) && !/\/node_modules\/expo\/(?:src|build)\/winter\//.test(id))
2558
- return null
2557
+ const isExpoWinter = /\/node_modules\/expo\/(?:src|build)\/winter\//.test(id)
2558
+ if (!isAppSource(id) && !isExpoWinter) return null
2559
2559
 
2560
- // prune upstream's disabled native branches before lifting module imports.
2561
- let result = /\/node_modules\/expo\/(?:src|build)\/winter\//.test(id)
2560
+ // prune disabled native branches before resolving their requires.
2561
+ let result = isExpoWinter
2562
2562
  ? minifySync(
2563
2563
  stripViteQuery(id),
2564
2564
  transformSync(stripViteQuery(id), code, { define: defines }).code,
@@ -2567,9 +2567,10 @@ function externalAppTransformPlugin(isAppSource: (p: string) => boolean): Plugin
2567
2567
  : code
2568
2568
  let imports = ''
2569
2569
  let importCounter = 0
2570
- let hasChanges = false
2570
+ let hasChanges = isExpoWinter
2571
2571
 
2572
- if (code.includes('require(')) {
2572
+ // rolldown keeps winter's lazy requires at their call sites in builds.
2573
+ if (code.includes('require(') && !(isBuild && isExpoWinter)) {
2573
2574
  const replacements: Array<{ start: number; end: number; varName: string }> = []
2574
2575
  const seenPaths = new Map<string, string>()
2575
2576
  const pending: unknown[] = [parseSync(stripViteQuery(id), result).program]
@@ -180,6 +180,8 @@ export interface XcodeAppTarget extends XcodeModuleTarget {
180
180
  command: [string, ...string[]]
181
181
  directory: string
182
182
  environment: Record<string, string>
183
+ inputPaths: string[]
184
+ inputFileListPaths: string[]
183
185
  }[]
184
186
  appIconName: string
185
187
  /** the app project PBXProject developmentRegion, xcode's "en" when it declares none. */
@@ -890,6 +892,8 @@ function compilationConditions(settings: (key: string) => string): string[] {
890
892
  ]
891
893
  }
892
894
 
895
+ export { expand as expandXcodeBuildSetting }
896
+
893
897
  function expand(value: string, lookup: (name: string) => string): string {
894
898
  return value.replace(
895
899
  /\$(?:\(([A-Za-z_][A-Za-z0-9_]*)((?::[^)]*)?)\)|\{([A-Za-z_][A-Za-z0-9_]*)((?::[^}]*)?)\}|([A-Za-z_][A-Za-z0-9_]*))/g,
@@ -1013,6 +1017,8 @@ function shellCopies(
1013
1017
  }
1014
1018
 
1015
1019
  // an xcfilelist names one path per line; blank lines and comments are skipped.
1020
+ export { fileListEntries as xcodeFileListEntries }
1021
+
1016
1022
  function fileListEntries(file: string, absolute: (raw: string) => string): string[] {
1017
1023
  return fs
1018
1024
  .readFileSync(file, 'utf8')
@@ -2851,8 +2857,6 @@ export function readXcodeAppTarget(
2851
2857
  const phase = app.project.object(id)
2852
2858
  if (
2853
2859
  phase?.isa !== 'PBXShellScriptBuildPhase' ||
2854
- list(phase.inputPaths).length ||
2855
- list(phase.inputFileListPaths).length ||
2856
2860
  list(phase.outputPaths).length ||
2857
2861
  list(phase.outputFileListPaths).length
2858
2862
  )
@@ -2864,6 +2868,8 @@ export function readXcodeAppTarget(
2864
2868
  str(phase.shellScript) ?? '',
2865
2869
  ),
2866
2870
  directory: app.project.projectDir,
2871
+ inputPaths: list(phase.inputPaths),
2872
+ inputFileListPaths: list(phase.inputFileListPaths),
2867
2873
  environment: Object.fromEntries(
2868
2874
  [...settingKeys]
2869
2875
  .filter((key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))