peachsim 0.1.1635 → 0.1.1640

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 (129) hide show
  1. package/README.md +5 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +62 -13
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/commands/doctor.ts +13 -5
  7. package/cli/commands/flow.ts +1 -1
  8. package/cli/commands/inspect/shared.ts +9 -4
  9. package/cli/commands/inspect.ts +65 -47
  10. package/cli/commands/preview.ts +1 -1
  11. package/cli/commands/record.ts +97 -47
  12. package/cli/commands/swift.ts +9 -3
  13. package/cli/commands/upload.ts +48 -42
  14. package/cli/drivers/playwright-sim-host.ts +65 -42
  15. package/cli/main.ts +1 -1
  16. package/cli/outbound-endpoints.ts +7 -5
  17. package/cli/pause-aware-timeout.ts +4 -0
  18. package/cli/ws-bridge.ts +19 -10
  19. package/detox/attempt-events.cjs +62 -5
  20. package/detox/attempt-events.d.cts +7 -0
  21. package/detox/expectations.ts +7 -3
  22. package/detox/index.ts +121 -47
  23. package/detox/motion-change.cjs +170 -21
  24. package/detox/motion-change.d.cts +32 -0
  25. package/detox/navigation-lifecycle.ts +35 -3
  26. package/dist-lib/agent-daemon-client.cjs +1 -1
  27. package/dist-lib/agent-events.cjs +1 -1
  28. package/dist-lib/attached-projects.cjs +1 -1
  29. package/dist-lib/auth/shared-session.cjs +1 -1
  30. package/dist-lib/backend-origin.cjs +8 -7
  31. package/dist-lib/beta.cjs +1 -1
  32. package/dist-lib/beta.mjs +1 -1
  33. package/dist-lib/bridge-constants.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.mjs +1 -1
  36. package/dist-lib/bridge-contract.cjs +1 -1
  37. package/dist-lib/bridge-contract.mjs +1 -1
  38. package/dist-lib/bundle-channel.cjs +4038 -1983
  39. package/dist-lib/capture-contract.cjs +1 -1
  40. package/dist-lib/capture-contract.mjs +1 -1
  41. package/dist-lib/cloud-contract.cjs +1 -1
  42. package/dist-lib/cloud-contract.mjs +1 -1
  43. package/dist-lib/cloud-test-args.cjs +1 -1
  44. package/dist-lib/cloud-test-args.mjs +1 -1
  45. package/dist-lib/cloud.cjs +1 -1
  46. package/dist-lib/cloud.mjs +1 -1
  47. package/dist-lib/config.cjs +1 -1
  48. package/dist-lib/config.mjs +1 -1
  49. package/dist-lib/detox/index.cjs +509 -86
  50. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  51. package/dist-lib/expo-swift-bundle.cjs +3551 -1574
  52. package/dist-lib/expo-swift-metadata.cjs +9 -7
  53. package/dist-lib/expo-swift-metadata.mjs +9 -7
  54. package/dist-lib/home-paths.cjs +1 -1
  55. package/dist-lib/host/bridge-host.cjs +43633 -2635
  56. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  59. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  60. package/dist-lib/host/websocket-proxy.cjs +1 -1
  61. package/dist-lib/index.cjs +2748 -128
  62. package/dist-lib/jump-to-source-babel.cjs +1 -1
  63. package/dist-lib/jump-to-source-native.cjs +1 -1
  64. package/dist-lib/kotlin-host.cjs +1 -1
  65. package/dist-lib/menu.cjs +1 -1
  66. package/dist-lib/menu.mjs +1 -1
  67. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  69. package/dist-lib/metro-production-bundle.cjs +1 -1
  70. package/dist-lib/metro-production-bundle.mjs +1 -1
  71. package/dist-lib/metro.cjs +2547 -83
  72. package/dist-lib/native-app-identity-project.cjs +237 -50
  73. package/dist-lib/native-app-identity-project.mjs +237 -50
  74. package/dist-lib/native-app-identity.cjs +29 -30
  75. package/dist-lib/native-app-identity.mjs +29 -30
  76. package/dist-lib/native-firebase-metadata.cjs +1 -1
  77. package/dist-lib/native-firebase-metadata.mjs +1 -1
  78. package/dist-lib/plist.cjs +1 -1
  79. package/dist-lib/plist.mjs +1 -1
  80. package/dist-lib/profiles.cjs +1 -1
  81. package/dist-lib/public-brand.cjs +1 -1
  82. package/dist-lib/react-native-host-modules.cjs +2 -2
  83. package/dist-lib/react-native-host-modules.mjs +2 -2
  84. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  85. package/dist-lib/sdk.cjs +1 -1
  86. package/dist-lib/sdk.mjs +1 -1
  87. package/dist-lib/skills.cjs +65403 -22739
  88. package/dist-lib/swift-compile.cjs +93 -32
  89. package/dist-lib/swift-compile.mjs +93 -32
  90. package/dist-lib/swift-package.cjs +1 -1
  91. package/dist-lib/swift-package.mjs +1 -1
  92. package/dist-lib/swift-submit.cjs +7 -5
  93. package/dist-lib/swift-submit.mjs +7 -5
  94. package/dist-lib/swift-toolchain.cjs +316 -41
  95. package/dist-lib/swift.cjs +1 -1
  96. package/dist-lib/vite-swift.cjs +7335 -5432
  97. package/dist-lib/vite.cjs +2857 -12
  98. package/dist-lib/worklet-executable.cjs +14 -2
  99. package/dist-lib/worklet-executable.mjs +14 -2
  100. package/package.json +1 -1
  101. package/src/backend-origin.ts +13 -6
  102. package/src/expo-swift-host.ts +5 -2
  103. package/src/expoKotlinContract.ts +4 -7
  104. package/src/expoSwiftContract.ts +4 -6
  105. package/src/host/staged-host-files.ts +9 -0
  106. package/src/native-app-identity-project.ts +1 -4
  107. package/src/native-app-identity.ts +1 -4
  108. package/src/nativeAppIconContract.ts +1 -4
  109. package/src/output-lock-stdio.ts +36 -0
  110. package/src/react-native-bridge-modules.ts +21 -6
  111. package/src/react-native-host-modules.ts +4 -1
  112. package/src/settings-bundle.ts +1 -4
  113. package/src/swift-compile.ts +91 -33
  114. package/src/swift-distribution.lock.json +22 -12
  115. package/src/swift-distribution.ts +103 -4
  116. package/src/swift-host.ts +89 -26
  117. package/src/swift-icu.ts +16 -10
  118. package/src/swift-objc-compile.ts +355 -0
  119. package/src/swift-objc-sdk.ts +689 -18
  120. package/src/swift-objc-standin.ts +126 -14
  121. package/src/swift-package-manifest.ts +39 -1
  122. package/src/swift-packages.ts +35 -21
  123. package/src/swift-standin-emit.ts +40 -7
  124. package/src/swift-standins.ts +404 -138
  125. package/src/swift-submit.ts +3 -5
  126. package/src/swift-toolchain.ts +378 -46
  127. package/src/worker-system-chunks.ts +11 -12
  128. package/src/worklet-executable.ts +19 -1
  129. package/src/xcode-project.ts +270 -30
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1640 | (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;
@@ -185,6 +185,8 @@ function enter(node, scope) {
185
185
  const name = identifierName(id);
186
186
  if (name !== void 0 && isNode(init))
187
187
  declare(target, name, { kind: "value", init, scope });
188
+ else if (name !== void 0 && init === null)
189
+ declare(target, name, { kind: "uninitialized" });
188
190
  else declareOpaque(target, patternNames(id));
189
191
  }
190
192
  return scope;
@@ -254,12 +256,18 @@ function collectInitData(root) {
254
256
  immediate: /* @__PURE__ */ new Map(),
255
257
  writes: [],
256
258
  reassigned: /* @__PURE__ */ new Set(),
259
+ assigned: /* @__PURE__ */ new Map(),
257
260
  free: /* @__PURE__ */ new Map()
258
261
  };
259
262
  visit(root, newScope(void 0, true), collection);
260
263
  for (const { name, scope, value } of collection.writes) {
261
264
  const binding = lookup(scope, name);
262
- if (binding) collection.reassigned.add(binding);
265
+ if (binding?.kind === "uninitialized")
266
+ collection.assigned.set(
267
+ binding,
268
+ collection.assigned.has(binding) || !value ? null : { value, scope }
269
+ );
270
+ else if (binding) collection.reassigned.add(binding);
263
271
  else
264
272
  collection.free.set(
265
273
  name,
@@ -320,6 +328,10 @@ function resolveObject(expression, scope, collection, depth) {
320
328
  return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
321
329
  }
322
330
  if (collection.reassigned.has(binding)) return void 0;
331
+ if (binding.kind === "uninitialized") {
332
+ const assigned = collection.assigned.get(binding);
333
+ return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
334
+ }
323
335
  if (binding.kind === "value")
324
336
  return resolveObject(binding.init, binding.scope, collection, depth + 1);
325
337
  if (binding.kind === "param") {
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/worklet-executable.ts
4
4
  import { parse } from "acorn";
@@ -160,6 +160,8 @@ function enter(node, scope) {
160
160
  const name = identifierName(id);
161
161
  if (name !== void 0 && isNode(init))
162
162
  declare(target, name, { kind: "value", init, scope });
163
+ else if (name !== void 0 && init === null)
164
+ declare(target, name, { kind: "uninitialized" });
163
165
  else declareOpaque(target, patternNames(id));
164
166
  }
165
167
  return scope;
@@ -229,12 +231,18 @@ function collectInitData(root) {
229
231
  immediate: /* @__PURE__ */ new Map(),
230
232
  writes: [],
231
233
  reassigned: /* @__PURE__ */ new Set(),
234
+ assigned: /* @__PURE__ */ new Map(),
232
235
  free: /* @__PURE__ */ new Map()
233
236
  };
234
237
  visit(root, newScope(void 0, true), collection);
235
238
  for (const { name, scope, value } of collection.writes) {
236
239
  const binding = lookup(scope, name);
237
- if (binding) collection.reassigned.add(binding);
240
+ if (binding?.kind === "uninitialized")
241
+ collection.assigned.set(
242
+ binding,
243
+ collection.assigned.has(binding) || !value ? null : { value, scope }
244
+ );
245
+ else if (binding) collection.reassigned.add(binding);
238
246
  else
239
247
  collection.free.set(
240
248
  name,
@@ -295,6 +303,10 @@ function resolveObject(expression, scope, collection, depth) {
295
303
  return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
296
304
  }
297
305
  if (collection.reassigned.has(binding)) return void 0;
306
+ if (binding.kind === "uninitialized") {
307
+ const assigned = collection.assigned.get(binding);
308
+ return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
309
+ }
298
310
  if (binding.kind === "value")
299
311
  return resolveObject(binding.init, binding.scope, collection, depth + 1);
300
312
  if (binding.kind === "param") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.1635",
3
+ "version": "0.1.1640",
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",
@@ -98,12 +98,16 @@ export function isBrowserAssetProxyUrl(value: string): boolean {
98
98
  }
99
99
  }
100
100
 
101
+ // the PeachSim app's host; the backend serves every path on it
102
+ export const PEACHSIM_NATIVE_HOST = 'native.peachsim.com'
103
+
101
104
  export function isContrastBackendHost(hostname: string): boolean {
102
105
  const normalized = normalizeHostname(hostname)
103
106
  return (
104
107
  isLoopbackHost(normalized) ||
105
108
  normalized === 'contrast.dev' ||
106
- normalized.endsWith('.contrast.dev')
109
+ normalized.endsWith('.contrast.dev') ||
110
+ normalized === PEACHSIM_NATIVE_HOST
107
111
  )
108
112
  }
109
113
 
@@ -115,15 +119,18 @@ export function isContrastBackendOrigin(origin: string): boolean {
115
119
  }
116
120
  }
117
121
 
118
- export function resolveContrastBackendOrigin(currentOrigin?: string | null): string {
119
- const origin = currentOrigin?.trim()
122
+ // a page's `?backend=` names another backend origin, judged by the same rule
123
+ export function resolveContrastBackendOrigin(
124
+ currentOrigin?: string | null,
125
+ search?: string | null,
126
+ ): string {
127
+ const origin =
128
+ new URLSearchParams(search ?? '').get('backend')?.trim() || currentOrigin?.trim()
120
129
  if (!origin) return DEFAULT_CONTRAST_BACKEND_ORIGIN
121
130
 
122
131
  try {
123
132
  const parsed = new URL(origin)
124
- if (isLoopbackHost(parsed.hostname) || isContrastBackendHost(parsed.hostname)) {
125
- return parsed.origin
126
- }
133
+ if (isContrastBackendHost(parsed.hostname)) return parsed.origin
127
134
  } catch {}
128
135
 
129
136
  return DEFAULT_CONTRAST_BACKEND_ORIGIN
@@ -136,7 +136,10 @@ export async function collectExpoSwiftSources(
136
136
  let bytes = 0
137
137
  let fileCount = 0
138
138
  // native Expo resolves packages by name before generating its Apple provider.
139
- for (const [, entry] of Object.entries(search).sort(([a], [b]) => a.localeCompare(b))) {
139
+ // the search result is keyed by that name; older autolinkers add no name field.
140
+ for (const [packageName, entry] of Object.entries(search).sort(([a], [b]) =>
141
+ a.localeCompare(b),
142
+ )) {
140
143
  if (!entry || typeof entry !== 'object')
141
144
  throw new Error('invalid Expo autolinking entry')
142
145
  const sourceRoot = Reflect.get(entry, 'path')
@@ -149,7 +152,7 @@ export async function collectExpoSwiftSources(
149
152
  (candidate) =>
150
153
  candidate &&
151
154
  typeof candidate === 'object' &&
152
- Reflect.get(candidate, 'packageName') === Reflect.get(entry, 'name'),
155
+ Reflect.get(candidate, 'packageName') === packageName,
153
156
  )
154
157
  const namespaces =
155
158
  resolution && typeof resolution === 'object'
@@ -1,4 +1,5 @@
1
1
  import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
2
+ import { isRecord } from './is-record'
2
3
 
3
4
  // what the dev server answers for an app's local Expo Kotlin modules on an
4
5
  // Android device: one linked Kotlin/Wasm process, or each module's diagnostic.
@@ -10,14 +11,10 @@ export interface ExpoKotlinCompilationResult {
10
11
  failures: { namespace: string; diagnostic: string }[]
11
12
  }
12
13
 
13
- function record(value: unknown): value is Record<string, unknown> {
14
- return value !== null && typeof value === 'object' && !Array.isArray(value)
15
- }
16
-
17
14
  export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationResult {
18
15
  const invalid = () => new Error('invalid Expo Kotlin compilation metadata')
19
16
  if (
20
- !record(value) ||
17
+ !isRecord(value) ||
21
18
  !Array.isArray(value.namespaces) ||
22
19
  !Array.isArray(value.failures)
23
20
  )
@@ -30,7 +27,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
30
27
  const failures: ExpoKotlinCompilationResult['failures'] = []
31
28
  for (const failure of value.failures) {
32
29
  if (
33
- !record(failure) ||
30
+ !isRecord(failure) ||
34
31
  typeof failure.namespace !== 'string' ||
35
32
  typeof failure.diagnostic !== 'string'
36
33
  )
@@ -40,7 +37,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
40
37
  const module = value.module
41
38
  if (module === null) return { namespaces, module: null, failures }
42
39
  if (
43
- !record(module) ||
40
+ !isRecord(module) ||
44
41
  typeof module.url !== 'string' ||
45
42
  typeof module.hash !== 'string' ||
46
43
  typeof module.compose !== 'boolean'
@@ -1,4 +1,5 @@
1
1
  import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
2
+ import { isRecord } from './is-record'
2
3
 
3
4
  export interface ExpoSwiftPreparationOptions {
4
5
  signal?: AbortSignal
@@ -33,9 +34,6 @@ export interface ExpoSwiftCompilationResult {
33
34
  failures: { namespace: string; diagnostic: string }[]
34
35
  }
35
36
 
36
- function record(value: unknown): value is Record<string, unknown> {
37
- return value !== null && typeof value === 'object' && !Array.isArray(value)
38
- }
39
37
  function strings(value: unknown): value is string[] {
40
38
  return Array.isArray(value) && value.every((item) => typeof item === 'string')
41
39
  }
@@ -44,7 +42,7 @@ export function isExpoSwiftCompilationResult(
44
42
  value: unknown,
45
43
  ): value is ExpoSwiftCompilationResult {
46
44
  if (
47
- !record(value) ||
45
+ !isRecord(value) ||
48
46
  !strings(value.namespaces) ||
49
47
  !Array.isArray(value.modules) ||
50
48
  value.modules.length > 1 ||
@@ -62,7 +60,7 @@ export function isExpoSwiftCompilationResult(
62
60
  return false
63
61
  for (const artifact of value.modules) {
64
62
  if (
65
- !record(artifact) ||
63
+ !isRecord(artifact) ||
66
64
  !strings(artifact.namespaces) ||
67
65
  artifact.namespaces.length === 0 ||
68
66
  !artifact.namespaces.every((namespace) => remaining.delete(namespace)) ||
@@ -75,7 +73,7 @@ export function isExpoSwiftCompilationResult(
75
73
  }
76
74
  for (const failure of value.failures) {
77
75
  if (
78
- !record(failure) ||
76
+ !isRecord(failure) ||
79
77
  typeof failure.namespace !== 'string' ||
80
78
  !remaining.delete(failure.namespace) ||
81
79
  typeof failure.diagnostic !== 'string'
@@ -95,6 +95,15 @@ export async function handleStagedHostFileRequest(
95
95
  res.end('staged file not found')
96
96
  return
97
97
  }
98
+ if (req.method === 'OPTIONS') {
99
+ res.writeHead(204, {
100
+ 'Access-Control-Allow-Methods': 'GET, HEAD',
101
+ 'Access-Control-Allow-Headers':
102
+ req.headers['access-control-request-headers'] ?? '*',
103
+ })
104
+ res.end()
105
+ return
106
+ }
98
107
  if (req.method !== 'GET' && req.method !== 'HEAD') {
99
108
  res.writeHead(405, { Allow: 'GET, HEAD' })
100
109
  res.end('method not allowed')
@@ -2,6 +2,7 @@ import fs from 'node:fs'
2
2
  import { createRequire } from 'node:module'
3
3
  import path from 'node:path'
4
4
  import { androidMergedResourceValuesDirectory } from './android-app-identity'
5
+ import { isRecord } from './is-record'
5
6
  import {
6
7
  extractNativeAppIdentityFromFiles,
7
8
  type NativeAppIdentity,
@@ -17,10 +18,6 @@ import {
17
18
  type XcodeAppTarget,
18
19
  } from './xcode-project'
19
20
 
20
- function isRecord(value: unknown): value is Record<string, unknown> {
21
- return typeof value === 'object' && value !== null && !Array.isArray(value)
22
- }
23
-
24
21
  function nonEmptyString(value: unknown): string | undefined {
25
22
  return typeof value === 'string' && value.length > 0 ? value : undefined
26
23
  }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { nativeAppIdentityFromAndroidManifest } from './android-app-identity'
12
12
  import { findBundleFooter } from './bundleGlobalFooter'
13
+ import { isRecord } from './is-record'
13
14
  import {
14
15
  resolveNativeAppIconArtwork,
15
16
  isNativeAppIconArtwork,
@@ -99,10 +100,6 @@ interface NativeAppIdentityGlobals {
99
100
  __sootsim_native_app_identity?: NativeAppIdentity | null
100
101
  }
101
102
 
102
- function isRecord(value: unknown): value is Record<string, unknown> {
103
- return typeof value === 'object' && value !== null && !Array.isArray(value)
104
- }
105
-
106
103
  function nonEmptyString(value: unknown): string | null {
107
104
  return typeof value === 'string' && value.length > 0 ? value : null
108
105
  }
@@ -1,14 +1,11 @@
1
1
  import { isSwiftImageVariants, type SwiftImageVariants } from './assetCatalogContract'
2
+ import { isRecord } from './is-record'
2
3
 
3
4
  export interface NativeAppIconArtwork {
4
5
  primary?: SwiftImageVariants
5
6
  alternates: Record<string, SwiftImageVariants>
6
7
  }
7
8
 
8
- function isRecord(value: unknown): value is Record<string, unknown> {
9
- return typeof value === 'object' && value !== null && !Array.isArray(value)
10
- }
11
-
12
9
  export function isNativeAppIconArtwork(value: unknown): value is NativeAppIconArtwork {
13
10
  return (
14
11
  isRecord(value) &&
@@ -0,0 +1,36 @@
1
+ import { fstatSync } from 'node:fs'
2
+ import type { IOType, StdioOptions } from 'node:child_process'
3
+
4
+ // lock identities name actual inherited descriptors, never paths to reopen.
5
+ export function outputLockStdio(
6
+ standard: [IOType, IOType, IOType],
7
+ env: NodeJS.ProcessEnv = process.env,
8
+ ): StdioOptions {
9
+ const stdio: StdioOptions = [...standard]
10
+ for (const [key, value] of Object.entries(process.env)) {
11
+ if (key.startsWith('REMOTE_OUTPUT_LOCK_') && env[key] !== value)
12
+ throw new Error(`inherited output lock identity changed for ${key}`)
13
+ }
14
+ for (const [key, value] of Object.entries(env)) {
15
+ if (!key.startsWith('REMOTE_OUTPUT_LOCK_')) continue
16
+ const fd = Number(value)
17
+ if (
18
+ !/^REMOTE_OUTPUT_LOCK_[a-f0-9]{64}$/.test(key) ||
19
+ !value ||
20
+ !/^\d+$/.test(value) ||
21
+ !Number.isSafeInteger(fd) ||
22
+ fd < 3
23
+ )
24
+ throw new Error(`invalid inherited output lock descriptor for ${key}`)
25
+ try {
26
+ fstatSync(fd)
27
+ } catch (cause) {
28
+ throw new Error(`inherited output lock descriptor ${fd} unavailable for ${key}`, {
29
+ cause,
30
+ })
31
+ }
32
+ while (stdio.length <= fd) stdio.push('ignore')
33
+ stdio[fd] = fd
34
+ }
35
+ return stdio
36
+ }
@@ -326,6 +326,7 @@ export function collectBridgeModuleSources(
326
326
  signal?.throwIfAborted()
327
327
  const iosRoot = path.join(appRoot, 'ios')
328
328
  if (!fs.existsSync(iosRoot)) return null
329
+ const projectRoot = fs.realpathSync(appRoot)
329
330
  const projects = fs
330
331
  .readdirSync(iosRoot, { withFileTypes: true })
331
332
  .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
@@ -342,8 +343,23 @@ export function collectBridgeModuleSources(
342
343
  const app = readXcodeAppTarget(targets[0]!.project, targets[0]!.name, 'Release', {
343
344
  requireSources: false,
344
345
  })
345
- const own = (file: string) =>
346
- !file.split(path.sep).some((part) => part === 'Pods' || part === 'node_modules')
346
+ const own = (file: string) => {
347
+ const resolved = fs.realpathSync(file)
348
+ if (
349
+ [file, resolved].some((name) =>
350
+ name.split(path.sep).some((part) => part === 'Pods' || part === 'node_modules'),
351
+ )
352
+ )
353
+ return false
354
+ const relative = path.relative(projectRoot, resolved)
355
+ if (
356
+ relative === '..' ||
357
+ relative.startsWith(`..${path.sep}`) ||
358
+ path.isAbsolute(relative)
359
+ )
360
+ throw new Error(`${file}: app native source outside ${projectRoot}`)
361
+ return true
362
+ }
347
363
  const objc = app.allSources.filter((file) => /\.mm?$/.test(file) && own(file))
348
364
  if (objc.length === 0) return null
349
365
  // headers declare the superclasses; they sit beside the sources that import them
@@ -352,7 +368,8 @@ export function collectBridgeModuleSources(
352
368
  .readdirSync(dir)
353
369
  .filter((name) => name.endsWith('.h'))
354
370
  .sort()
355
- .map((name) => path.join(dir, name)),
371
+ .map((name) => path.join(dir, name))
372
+ .filter(own),
356
373
  )
357
374
  const declarations = parseObjectiveCModules(
358
375
  [...objc, ...headers].map((file) => ({ file, text: fs.readFileSync(file, 'utf8') })),
@@ -361,9 +378,7 @@ export function collectBridgeModuleSources(
361
378
  const namespace = app.moduleName.replace(/[^A-Za-z0-9_]/g, '_')
362
379
  const swiftFiles: Record<string, string> = {}
363
380
  for (const file of app.sources.filter(own)) {
364
- const relative = path.relative(iosRoot, file)
365
- if (relative.startsWith('..') || path.isAbsolute(relative))
366
- throw new Error(`${file}: app Swift source outside ${iosRoot}`)
381
+ const relative = path.relative(projectRoot, fs.realpathSync(file))
367
382
  swiftFiles[`Sources/${namespace}/${relative.split(path.sep).join('/')}`] =
368
383
  `#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
369
384
  }
@@ -43,7 +43,10 @@ export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
43
43
  ]
44
44
 
45
45
  export function isReactNativePassthroughSpecifier(specifier: string): boolean {
46
+ // checked in place: every module of a bundle asks at boot
46
47
  return REACT_NATIVE_PASSTHROUGH_SPECIFIERS.some(
47
- (passthrough) => specifier === passthrough || specifier.startsWith(`${passthrough}/`),
48
+ (passthrough) =>
49
+ specifier.startsWith(passthrough) &&
50
+ (specifier.length === passthrough.length || specifier[passthrough.length] === '/'),
48
51
  )
49
52
  }
@@ -1,5 +1,6 @@
1
1
  import { readFile, readdir, stat } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
+ import { isRecord } from './is-record'
3
4
  import { parsePlistSource } from './plist.ts'
4
5
  import type { AppPreferenceValue, AppSettingsSpecifier } from '@peach/globals'
5
6
 
@@ -22,10 +23,6 @@ interface CachedSettingsBundle {
22
23
 
23
24
  const settingsBundleCache = new Map<string, CachedSettingsBundle>()
24
25
 
25
- function isRecord(value: unknown): value is PlistRecord {
26
- return typeof value === 'object' && value !== null && !Array.isArray(value)
27
- }
28
-
29
26
  function optionalString(value: unknown): string | undefined {
30
27
  return typeof value === 'string' ? value : undefined
31
28
  }