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
@@ -0,0 +1,146 @@
1
+ // read only an app's completed manifest merger output. raw source manifests
2
+ // cannot name the selected variant, linked AARs, or the effects of merge rules.
3
+ import { compileAndroidStringValue } from './android-string-value'
4
+ import { parseXml } from './android-xml'
5
+ import type { AndroidManifestPermission, NativeAppIdentity } from './native-app-identity'
6
+
7
+ export function nativeAppIdentityFromAndroidManifest(
8
+ source: string,
9
+ file: string,
10
+ files: Readonly<Record<string, string>> = {},
11
+ ): NativeAppIdentity | null {
12
+ const document = parseXml(source, file)
13
+ if (document.children.length !== 1) return null
14
+ const manifest = document.children[0]!
15
+ if (manifest.name !== 'manifest') return null
16
+ const androidPrefix = Object.entries(manifest.attributes)
17
+ .find(
18
+ ([name, value]) =>
19
+ name.startsWith('xmlns:') &&
20
+ value === 'http://schemas.android.com/apk/res/android',
21
+ )?.[0]
22
+ .slice('xmlns:'.length)
23
+ if (!androidPrefix) return null
24
+ const attribute = `${androidPrefix}:`
25
+ const bundleIdentifier = manifest.attributes.package
26
+ const appVersion = manifest.attributes[`${attribute}versionName`]
27
+ const buildVersion = manifest.attributes[`${attribute}versionCode`]
28
+ const application = manifest.children.find((child) => child.name === 'application')
29
+ if (
30
+ !application ||
31
+ !bundleIdentifier ||
32
+ !appVersion ||
33
+ !buildVersion ||
34
+ !/^[1-9][0-9]*$/.test(buildVersion) ||
35
+ [bundleIdentifier, appVersion].some((value) => /\$\{|^[@?]/.test(value))
36
+ )
37
+ return null
38
+
39
+ // an unconsumed merger marker or placeholder is not a completed app output.
40
+ const toolsPrefixes = Object.entries(manifest.attributes)
41
+ .filter(
42
+ ([name, value]) =>
43
+ name.startsWith('xmlns:') && value === 'http://schemas.android.com/tools',
44
+ )
45
+ .map(([name]) => `${name.slice('xmlns:'.length)}:`)
46
+ const androidManifestPermissions: AndroidManifestPermission[] = []
47
+ for (const element of manifest.children) {
48
+ if (
49
+ !['uses-permission', 'uses-permission-sdk-23', 'uses-permission-sdk-m'].includes(
50
+ element.name,
51
+ )
52
+ )
53
+ continue
54
+ if (
55
+ Object.keys(element.attributes).some((key) =>
56
+ toolsPrefixes.some((prefix) => key.startsWith(prefix)),
57
+ )
58
+ )
59
+ return null
60
+ const name = element.attributes[`${attribute}name`]
61
+ const max = element.attributes[`${attribute}maxSdkVersion`]
62
+ // feature-dependent declarations need the installed device's feature set.
63
+ // resource references and unresolved placeholders cannot certify a permission.
64
+ if (
65
+ !name ||
66
+ /\$\{|^[@?]/.test(name) ||
67
+ element.attributes[`${attribute}requiredFeature`] !== undefined ||
68
+ element.attributes[`${attribute}requiredNotFeature`] !== undefined ||
69
+ (max !== undefined && !/^\d+$/.test(max))
70
+ )
71
+ return null
72
+ androidManifestPermissions.push({
73
+ name,
74
+ minSdkVersion: element.name === 'uses-permission' ? 1 : 23,
75
+ ...(max === undefined ? {} : { maxSdkVersion: Number(max) }),
76
+ })
77
+ }
78
+ let label = application.attributes[`${attribute}label`]
79
+ let applicationName: string | null = null
80
+ if (label && !/\$\{|^[@?]/.test(label)) applicationName = label
81
+ else if (label?.startsWith('@string/')) {
82
+ const resourceDirectory = androidMergedResourceValuesDirectory(file)
83
+ const resourceFile = resourceDirectory
84
+ ? `${resourceDirectory}/values/values.xml`
85
+ : null
86
+ const resources = resourceFile ? files[resourceFile] : undefined
87
+ if (resources !== undefined && resourceFile) {
88
+ const document = parseXml(resources, resourceFile)
89
+ const root = document.children[0]
90
+ if (document.children.length === 1 && root?.name === 'resources') {
91
+ const qualified = Object.entries(files)
92
+ .filter(
93
+ ([name]) =>
94
+ name.startsWith(`${resourceDirectory}/values-`) && name.endsWith('.xml'),
95
+ )
96
+ .flatMap(([name, source]) => parseXml(source, name).children)
97
+ .flatMap((root) => root.children)
98
+ const visited = new Set<string>()
99
+ while (label?.startsWith('@string/') && !visited.has(label)) {
100
+ visited.add(label)
101
+ const name = label.slice('@string/'.length)
102
+ // no device resource configuration travels with this build metadata.
103
+ // a qualified override makes a default label insufficient evidence.
104
+ if (qualified.some((element) => element.attributes.name === name)) break
105
+ const matches = root.children.filter(
106
+ (element) =>
107
+ (element.name === 'string' ||
108
+ (element.name === 'item' && element.attributes.type === 'string')) &&
109
+ element.attributes.name === name,
110
+ )
111
+ const value = matches.length === 1 ? matches[0] : undefined
112
+ // styled or unresolved values cannot certify a plain application label.
113
+ if (!value || value.children.length) break
114
+ label = value.text.trim()
115
+ if (/^[@?]/.test(label)) continue
116
+ applicationName = compileAndroidStringValue(value.text, resourceFile) || null
117
+ break
118
+ }
119
+ }
120
+ }
121
+ }
122
+ return {
123
+ applicationName,
124
+ appVersion,
125
+ buildVersion,
126
+ bundleIdentifier,
127
+ platform: 'android',
128
+ androidManifestPermissions,
129
+ }
130
+ }
131
+
132
+ // AGP's resource merger writes the selected variant's uncompiled values here.
133
+ // raw src/main resources cannot certify flavor, build type or library overrides.
134
+ export function androidMergedResourceValuesDirectory(
135
+ manifestFile: string,
136
+ ): string | null {
137
+ const match =
138
+ /^(.*\/build\/intermediates\/)merged_manifests\/([^/]+)\/[^/]+\/AndroidManifest\.xml$/.exec(
139
+ manifestFile,
140
+ )
141
+ if (!match) return null
142
+ const [, intermediates, variant] = match
143
+ if (!variant) return null
144
+ const taskVariant = variant[0]!.toUpperCase() + variant.slice(1)
145
+ return `${intermediates}incremental/${variant}/merge${taskVariant}Resources/merged.dir`
146
+ }
@@ -0,0 +1,75 @@
1
+ // use the app's Expo resolver so transitive dependencies, exclusions and
2
+ // plugin-only Android packages follow the same autolinking as Gradle.
3
+ import { execFile } from 'node:child_process'
4
+ import fs from 'node:fs'
5
+ import { createRequire } from 'node:module'
6
+ import path from 'node:path'
7
+ import { promisify } from 'node:util'
8
+ import { RNX_FORKED_SCRIPT_ENV } from '../cli/internal-child'
9
+ import { readAndroidAutolinkedPackages } from './androidAutolinkingContract'
10
+ import { isRecord } from './is-record'
11
+
12
+ const run = promisify(execFile)
13
+
14
+ const resolving = new Map<string, Promise<readonly Record<string, unknown>[]>>()
15
+
16
+ // only an overlapping resolution pass is shared. later builds must observe
17
+ // changed dependencies, module configuration and autolinking exclusions.
18
+ export async function resolveExpoAndroidAutolinking(
19
+ appRoot: string,
20
+ ): Promise<readonly Record<string, unknown>[]> {
21
+ const root = fs.realpathSync(appRoot)
22
+ const existing = resolving.get(root)
23
+ if (existing) return existing
24
+ const pending = resolveAndroidModules(root)
25
+ resolving.set(root, pending)
26
+ try {
27
+ return await pending
28
+ } finally {
29
+ if (resolving.get(root) === pending) resolving.delete(root)
30
+ }
31
+ }
32
+
33
+ async function resolveAndroidModules(
34
+ appRoot: string,
35
+ ): Promise<readonly Record<string, unknown>[]> {
36
+ let autolinker: string
37
+ try {
38
+ autolinker = createRequire(path.join(appRoot, 'package.json')).resolve(
39
+ 'expo-modules-autolinking/package.json',
40
+ )
41
+ } catch (error) {
42
+ if (isRecord(error) && error.code === 'MODULE_NOT_FOUND') return []
43
+ throw error
44
+ }
45
+ const { stdout } = await run(
46
+ process.execPath,
47
+ [
48
+ path.join(path.dirname(autolinker), 'bin/expo-modules-autolinking.js'),
49
+ 'resolve',
50
+ '--platform',
51
+ 'android',
52
+ '--project-root',
53
+ appRoot,
54
+ '--json',
55
+ ],
56
+ {
57
+ timeout: 30_000,
58
+ maxBuffer: 8 * 1024 * 1024,
59
+ env: { ...process.env, [RNX_FORKED_SCRIPT_ENV]: '1' },
60
+ },
61
+ )
62
+ const resolved: unknown = JSON.parse(stdout)
63
+ if (!isRecord(resolved) || !Array.isArray(resolved.modules))
64
+ throw new Error('invalid Expo Android autolinking result')
65
+ if (!resolved.modules.every(isRecord))
66
+ throw new Error('invalid Expo Android autolinking entry')
67
+ return resolved.modules
68
+ }
69
+
70
+ export async function collectAndroidAutolinkedPackages(
71
+ appRoot: string,
72
+ ): Promise<string[]> {
73
+ const modules = await resolveExpoAndroidAutolinking(appRoot)
74
+ return readAndroidAutolinkedPackages(modules.map((entry) => entry.packageName))
75
+ }
@@ -0,0 +1,36 @@
1
+ // aapt2's StringBuilder over the decoded text: outside double quotes a run of
2
+ // whitespace is one space and the ends are trimmed, an unescaped apostrophe is
3
+ // an error; a double quote toggles quoting and is dropped; a backslash escapes
4
+ // the next character (\n, \t and \uXXXX, any other character as itself). a
5
+ // value beginning with @ or ? must be resolved before compiling its text
6
+ export function compileAndroidStringValue(text: string, where: string): string {
7
+ if (/^\s*[@?]/.test(text))
8
+ throw new Error(`${where} is a resource reference, not resolved by peach kotlin yet`)
9
+ const out: { char: string; soft: boolean }[] = []
10
+ let quoted = false
11
+ for (let index = 0; index < text.length; index++) {
12
+ const char = text[index] ?? ''
13
+ if (char === '\\') {
14
+ const next = text[index + 1]
15
+ if (next === undefined) break
16
+ index++
17
+ if (next === 'n') out.push({ char: '\n', soft: false })
18
+ else if (next === 't') out.push({ char: '\t', soft: false })
19
+ else if (next === 'u') {
20
+ const hex = text.slice(index + 1, index + 5)
21
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
22
+ throw new Error(`${where} has an invalid unicode escape \\u${hex}`)
23
+ out.push({ char: String.fromCharCode(Number.parseInt(hex, 16)), soft: false })
24
+ index += 4
25
+ } else out.push({ char: next, soft: false })
26
+ } else if (char === '"') quoted = !quoted
27
+ else if (char === "'" && !quoted)
28
+ throw new Error(`${where} has an unescaped apostrophe`)
29
+ else if (!quoted && /\s/.test(char)) {
30
+ if (out.at(-1)?.soft !== true) out.push({ char: ' ', soft: true })
31
+ } else out.push({ char, soft: false })
32
+ }
33
+ while (out[0]?.soft) out.shift()
34
+ while (out.at(-1)?.soft) out.pop()
35
+ return out.map((part) => part.char).join('')
36
+ }
@@ -6,6 +6,7 @@
6
6
  // rejected by name, never read as the default or as text.
7
7
  import fs from 'node:fs'
8
8
  import path from 'node:path'
9
+ import { compileAndroidStringValue } from './android-string-value'
9
10
  import { decodeXmlText } from './android-xml'
10
11
 
11
12
  // one resource's value per locale: '' for values, then a language or a
@@ -123,41 +124,9 @@ const sorted = <Value>(
123
124
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
124
125
  .map(([name, values]) => ({ name, values }))
125
126
 
126
- // aapt2's StringBuilder over the decoded text: outside double quotes a run of
127
- // whitespace is one space and the ends are trimmed, an unescaped apostrophe is
128
- // an error; a double quote toggles quoting and is dropped; a backslash escapes
129
- // the next character (\n, \t and \uXXXX, any other character as itself). a
130
- // value beginning with @ or ? is a reference aapt2 resolves, which Peach does
131
- // not yet
132
127
  function compileString(raw: string, where: string): string {
133
- const text = decodeXmlText(raw.replace(/<[^>]*>/g, ''), where)
134
- if (/^\s*[@?]/.test(text))
135
- throw new Error(`${where} is a resource reference, not resolved by peach kotlin yet`)
136
- const out: { char: string; soft: boolean }[] = []
137
- let quoted = false
138
- for (let index = 0; index < text.length; index++) {
139
- const char = text[index] ?? ''
140
- if (char === '\\') {
141
- const next = text[index + 1]
142
- if (next === undefined) break
143
- index++
144
- if (next === 'n') out.push({ char: '\n', soft: false })
145
- else if (next === 't') out.push({ char: '\t', soft: false })
146
- else if (next === 'u') {
147
- const hex = text.slice(index + 1, index + 5)
148
- if (!/^[0-9a-fA-F]{4}$/.test(hex))
149
- throw new Error(`${where} has an invalid unicode escape \\u${hex}`)
150
- out.push({ char: String.fromCharCode(Number.parseInt(hex, 16)), soft: false })
151
- index += 4
152
- } else out.push({ char: next, soft: false })
153
- } else if (char === '"') quoted = !quoted
154
- else if (char === "'" && !quoted)
155
- throw new Error(`${where} has an unescaped apostrophe`)
156
- else if (!quoted && /\s/.test(char)) {
157
- if (out.at(-1)?.soft !== true) out.push({ char: ' ', soft: true })
158
- } else out.push({ char, soft: false })
159
- }
160
- while (out[0]?.soft) out.shift()
161
- while (out.at(-1)?.soft) out.pop()
162
- return out.map((part) => part.char).join('')
128
+ return compileAndroidStringValue(
129
+ decodeXmlText(raw.replace(/<[^>]*>/g, ''), where),
130
+ where,
131
+ )
163
132
  }
@@ -0,0 +1,40 @@
1
+ import {
2
+ detachBundleGlobalFooter,
3
+ insertBundleGlobalFooter,
4
+ readBundleGlobalFooter,
5
+ } from './bundleGlobalFooter'
6
+
7
+ const FOOTER = '__peachAndroidAutolinkedPackages'
8
+
9
+ export function readAndroidAutolinkedPackages(value: unknown): string[] {
10
+ if (
11
+ !Array.isArray(value) ||
12
+ !value.every((name) => typeof name === 'string' && name.length > 0)
13
+ )
14
+ throw new Error('invalid Android autolinked packages')
15
+ return [...new Set(value)].sort((left, right) => left.localeCompare(right))
16
+ }
17
+
18
+ export function readAndroidAutolinkingFooter(source: string): string[] | null {
19
+ const value = readBundleGlobalFooter(source, FOOTER)
20
+ return value === null ? null : readAndroidAutolinkedPackages(value)
21
+ }
22
+
23
+ export function insertAndroidAutolinkingFooter(
24
+ source: string,
25
+ packages: readonly string[],
26
+ ): string {
27
+ return insertBundleGlobalFooter(source, FOOTER, readAndroidAutolinkedPackages(packages))
28
+ }
29
+
30
+ export function detachAndroidAutolinkingFooter(source: string): {
31
+ source: string
32
+ packages: string[] | null
33
+ } {
34
+ const detached = detachBundleGlobalFooter(source, FOOTER)
35
+ return {
36
+ source: detached.source,
37
+ packages:
38
+ detached.value === null ? null : readAndroidAutolinkedPackages(detached.value),
39
+ }
40
+ }
@@ -6,6 +6,17 @@ import { PEACH_SIMULATOR_PATH } from './simulator-route'
6
6
  export const DEFAULT_SOOTSIM_BRIDGE_PORT = 7668
7
7
  export const DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/`
8
8
 
9
+ export function assertPeachPortEnvironment(env: Record<string, unknown>): void {
10
+ for (const [removed, replacement] of [
11
+ ['SOOTSIM_SHELL_PORT', 'PEACH_SHELL_PORT'],
12
+ ['SOOTSIM_BRIDGE_PORT', 'PEACH_BRIDGE_PORT'],
13
+ ] satisfies [string, string][]) {
14
+ if (env[removed] !== undefined) {
15
+ throw new Error(`${removed} has been removed; unset it and use ${replacement}`)
16
+ }
17
+ }
18
+ }
19
+
9
20
  function parsePort(value: unknown): number | null {
10
21
  if (typeof value === 'number') {
11
22
  return Number.isInteger(value) && value > 0 ? value : null
@@ -385,39 +385,29 @@ export const SEMANTIC_READY_POLL_MS = 150
385
385
 
386
386
  export const HEADLESS_TENANT_FETCH_HUNG_MARKER = 'headless tenant fetch hung'
387
387
 
388
- export function readyProbeHasContent(
389
- probe: Pick<SemanticReadyProbe, 'nodes' | 'targets'> &
390
- Partial<
391
- Pick<
392
- SemanticReadyProbe,
393
- 'liveFrameActive' | 'browserSurfaceCount' | 'paintedAppContent'
394
- >
395
- >,
396
- ): boolean {
397
- return (
398
- probe.paintedAppContent === true ||
399
- (probe.browserSurfaceCount ?? 0) > 0 ||
400
- probe.liveFrameActive ||
401
- probe.targets > 0 ||
402
- probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR
403
- )
388
+ type ReadyContentProbe = Pick<SemanticReadyProbe, 'nodes' | 'targets'> &
389
+ Partial<
390
+ Pick<
391
+ SemanticReadyProbe,
392
+ 'liveFrameActive' | 'browserSurfaceCount' | 'paintedAppContent' | 'flag'
393
+ >
394
+ >
395
+
396
+ // the first content condition a probe satisfies. a probe without the ready
397
+ // flag counts only targeted content, never the node floor. empty when the
398
+ // probe has none.
399
+ export function readyProbeContentBasis(probe: ReadyContentProbe): string {
400
+ if (probe.paintedAppContent === true) return 'paintedAppContent'
401
+ if ((probe.browserSurfaceCount ?? 0) > 0) return 'browserSurface'
402
+ if (probe.liveFrameActive) return 'liveFrame'
403
+ if (probe.targets > 0) return 'targets'
404
+ if (probe.flag === true && probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR)
405
+ return 'nodeFloor'
406
+ return ''
404
407
  }
405
408
 
406
- export function readyProbeHasTargetContent(
407
- probe: Pick<SemanticReadyProbe, 'targets'> &
408
- Partial<
409
- Pick<
410
- SemanticReadyProbe,
411
- 'liveFrameActive' | 'browserSurfaceCount' | 'paintedAppContent'
412
- >
413
- >,
414
- ): boolean {
415
- return (
416
- probe.paintedAppContent === true ||
417
- (probe.browserSurfaceCount ?? 0) > 0 ||
418
- probe.liveFrameActive ||
419
- probe.targets > 0
420
- )
409
+ export function readyProbeHasContent(probe: ReadyContentProbe): boolean {
410
+ return readyProbeContentBasis({ ...probe, flag: true }) !== ''
421
411
  }
422
412
 
423
413
  // one probe shows a ready app: it has said so with content behind it, or it
@@ -426,9 +416,7 @@ export function readyProbeHasTargetContent(
426
416
  // already let the app settle judges a single probe with this alone.
427
417
  export function readyProbeIsReady(probe: SemanticReadyProbe): boolean {
428
418
  return (
429
- (probe.flag === true
430
- ? readyProbeHasContent(probe)
431
- : readyProbeHasTargetContent(probe)) &&
419
+ readyProbeContentBasis(probe) !== '' &&
432
420
  probe.externalReady !== false &&
433
421
  !probe.externalError &&
434
422
  !probe.loadingText
@@ -14,18 +14,37 @@ export function findBundleFooter(source: string, prefix: string): number {
14
14
  if (newline < 0) return -1
15
15
  const line = newline + 1
16
16
  if (source.startsWith(prefix, line)) return newline
17
- if (
18
- line !== end &&
19
- !source.startsWith('globalThis.', line) &&
20
- !source.startsWith('//', line)
21
- ) {
22
- return -1
23
- }
17
+ if (!isFooterBlockLine(source, line, end)) return -1
24
18
  end = newline
25
19
  }
26
20
  return -1
27
21
  }
28
22
 
23
+ function isFooterBlockLine(source: string, line: number, end: number): boolean {
24
+ return (
25
+ line === end ||
26
+ source.startsWith('globalThis.', line) ||
27
+ source.startsWith('//', line)
28
+ )
29
+ }
30
+
31
+ // the whole trailing block the readers above walk, split from the code before
32
+ // it. a rewrite of the code (minification) must leave the block as written:
33
+ // a minifier reprints each footer's JSON as an object literal and moves legal
34
+ // comments after it, so no reader would find the footer again.
35
+ export function splitBundleFooterBlock(source: string): {
36
+ code: string
37
+ footers: string
38
+ } {
39
+ let end = source.length
40
+ while (end > 0) {
41
+ const newline = source.lastIndexOf('\n', end - 1)
42
+ if (newline < 0 || !isFooterBlockLine(source, newline + 1, end)) break
43
+ end = newline
44
+ }
45
+ return { code: source.slice(0, end), footers: source.slice(end) }
46
+ }
47
+
29
48
  function readFooter(
30
49
  source: string,
31
50
  prefix: string,
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
11
11
  import readline from 'node:readline'
12
+ import { terminateProcessGroup } from './process-group'
12
13
 
13
14
  export interface CodexClientOptions {
14
15
  /** resolved path to the codex binary (via `which codex` or user override). */
@@ -211,10 +212,10 @@ export function spawnCodexClient(options: CodexClientOptions): CodexClient {
211
212
  try {
212
213
  child.stdin.end()
213
214
  } catch {}
214
- const t = setTimeout(() => {
215
+ const t = setTimeout(async () => {
215
216
  if (!closed) {
216
217
  try {
217
- child.kill('SIGTERM')
218
+ await terminateProcessGroup(child, 'SIGTERM')
218
219
  } catch {}
219
220
  }
220
221
  }, timeoutMs)
package/src/config.ts CHANGED
@@ -76,6 +76,8 @@ export interface PeachConfig<Settings extends Record<string, any> = Record<strin
76
76
  runtimeVersion?: string
77
77
  // native host identity. omission preserves the Fabric default.
78
78
  nativeRenderer?: NativeRenderer
79
+ // developer-owned browser token, restricted to the runtime's origin.
80
+ mapKit?: { token: string }
79
81
  modules?: Record<string, ModuleResolution>
80
82
  // native stubs keyed by the package they answer. the cli lowers these, with
81
83
  // installed stub packages, into the wire slots below before anything runs.
@@ -95,6 +97,9 @@ export interface PeachConfig<Settings extends Record<string, any> = Record<strin
95
97
  remap?: Record<string, string>
96
98
  }
97
99
  settings?: Partial<Settings>
100
+ // the properties a native host hands the root component at launch, the
101
+ // initialProperties of RCTRootView / runApplication's initialProps.
102
+ initialProps?: Record<string, unknown>
98
103
  initialState?: {
99
104
  authenticated?: boolean
100
105
  locale?: string
@@ -131,6 +136,11 @@ export function isPeachConfig<Settings extends Record<string, any> = Record<stri
131
136
  (value.nativeRenderer === undefined ||
132
137
  value.nativeRenderer === 'paper' ||
133
138
  value.nativeRenderer === 'fabric') &&
139
+ (value.mapKit === undefined ||
140
+ (isRecord(value.mapKit) &&
141
+ typeof value.mapKit.token === 'string' &&
142
+ value.mapKit.token.length > 0 &&
143
+ value.mapKit.token === value.mapKit.token.trim())) &&
134
144
  (value.runtimeVersion === undefined ||
135
145
  (typeof value.runtimeVersion === 'string' &&
136
146
  value.runtimeVersion === value.runtimeVersion.trim() &&
@@ -144,6 +154,7 @@ export function isPeachConfig<Settings extends Record<string, any> = Record<stri
144
154
  'env',
145
155
  'network',
146
156
  'settings',
157
+ 'initialProps',
147
158
  'initialState',
148
159
  ].every((key) => value[key] === undefined || isRecord(value[key])) &&
149
160
  (value.packages === undefined ||
@@ -232,6 +243,7 @@ export function hasPeachConfig<
232
243
  if (!config) return false
233
244
  return (
234
245
  config.nativeRenderer !== undefined ||
246
+ config.mapKit !== undefined ||
235
247
  (typeof config.runtimeVersion === 'string' && config.runtimeVersion.length > 0) ||
236
248
  hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
237
249
  hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
@@ -240,6 +252,7 @@ export function hasPeachConfig<
240
252
  hasOwnKeys(config.env as Record<string, unknown> | undefined) ||
241
253
  hasOwnKeys(config.network?.remap as Record<string, unknown> | undefined) ||
242
254
  hasOwnKeys(config.settings as Record<string, unknown> | undefined) ||
255
+ hasOwnKeys(config.initialProps) ||
243
256
  hasOwnKeys(config.initialState as Record<string, unknown> | undefined)
244
257
  )
245
258
  }
@@ -257,6 +270,7 @@ export function mergePeachConfig<
257
270
  return {
258
271
  runtimeVersion: override.runtimeVersion ?? base.runtimeVersion,
259
272
  nativeRenderer: override.nativeRenderer ?? base.nativeRenderer,
273
+ mapKit: override.mapKit ?? base.mapKit,
260
274
  modules: { ...(base.modules || {}), ...(override.modules || {}) },
261
275
  turboModules: { ...(base.turboModules || {}), ...(override.turboModules || {}) },
262
276
  nativeModules: {
@@ -272,6 +286,7 @@ export function mergePeachConfig<
272
286
  ...(base.settings || {}),
273
287
  ...(override.settings || {}),
274
288
  } as Partial<Settings>,
289
+ initialProps: { ...(base.initialProps || {}), ...(override.initialProps || {}) },
275
290
  initialState: { ...(base.initialState || {}), ...(override.initialState || {}) },
276
291
  }
277
292
  }
@@ -229,9 +229,13 @@ export function headlessHostCommand(): string[] | null {
229
229
  if (!fs.existsSync(launcher) || !hostEntrySupportsAutomaticPort(host)) return null
230
230
  return [launcher, '--experimental-vm-modules', host]
231
231
  }
232
- const version = placedHostVersion()
232
+ const pin = desktopHostDelivery().resolvePin()
233
+ if (pin && !/^\d+\.\d+$/.test(pin))
234
+ throw new Error(`invalid native host version '${pin}' (expected <build>.<attempt>)`)
235
+ const version = pin || placedHostVersion()
233
236
  if (!version) return null
234
237
  const dir = desktopHostDir(version)
238
+ if (!fs.existsSync(path.join(dir, desktopHostExecutable()))) return null
235
239
  // a placed host that predates the automatic-port bind upgrades like a missing one
236
240
  if (!hostEntrySupportsAutomaticPort(path.join(dir, 'node/host.mjs'))) return null
237
241
  return [