peachsim 0.1.669 → 0.1.671

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 (116) hide show
  1. package/cli/commands/control.ts +8 -5
  2. package/cli/commands/inspect/core.ts +7 -5
  3. package/cli/commands/inspect/wait-selector.ts +2 -11
  4. package/cli/commands/inspect.ts +6 -59
  5. package/cli/commands/kotlin.ts +6 -4
  6. package/cli/commands/skills.ts +28 -7
  7. package/cli/commands/upgrade.ts +18 -3
  8. package/cli/main.ts +21 -0
  9. package/cli/output-redaction.ts +182 -0
  10. package/cli/parse-args.ts +1 -0
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +1 -1
  22. package/dist-lib/bridge-contract-input.mjs +1 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +1 -1
  31. package/dist-lib/cloud-test-args.mjs +1 -1
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +1 -1
  35. package/dist-lib/detox/index.cjs +1 -1
  36. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  37. package/dist-lib/home-paths.cjs +1 -1
  38. package/dist-lib/host/bridge-host.cjs +30 -22
  39. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  40. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  42. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +260 -17
  45. package/dist-lib/jump-to-source-babel.cjs +1 -1
  46. package/dist-lib/jump-to-source-native.cjs +1 -1
  47. package/dist-lib/kotlin-host.cjs +2 -1
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +259 -16
  55. package/dist-lib/native-app-identity.cjs +1 -1
  56. package/dist-lib/native-app-identity.mjs +1 -1
  57. package/dist-lib/native-firebase-metadata.cjs +115 -0
  58. package/dist-lib/native-firebase-metadata.mjs +86 -0
  59. package/dist-lib/plist.cjs +1 -1
  60. package/dist-lib/plist.mjs +1 -1
  61. package/dist-lib/profiles.cjs +1 -1
  62. package/dist-lib/public-brand.cjs +1 -1
  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/render-mode.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +2 -2
  68. package/dist-lib/sdk.mjs +2 -2
  69. package/dist-lib/skills.cjs +480 -206
  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.cjs +1 -1
  77. package/dist-lib/vite-swift.cjs +21 -13
  78. package/dist-lib/vite.cjs +1 -1
  79. package/kotlin/AppSources/androidx/activity/ActivityFlags.kt +9 -0
  80. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +25 -4
  81. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +15 -4
  82. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +12 -8
  83. package/kotlin/AppSources/androidx/activity/ExperimentalActivityApi.kt +6 -0
  84. package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +50 -9
  85. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +67 -89
  86. package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -24
  87. package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerCompat.kt +38 -0
  88. package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerDispatcherCompat.kt +27 -0
  89. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +5 -5
  90. package/kotlin/ComposeSources/PeachComposeHost.kt +31 -7
  91. package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +1 -1
  92. package/kotlin/fixtures/kotlin-app-variants/Browse.kt +73 -0
  93. package/kotlin/fixtures/kotlin-app-variants/Focus.kt +62 -0
  94. package/kotlin/fixtures/kotlin-app-variants/Lists.kt +57 -0
  95. package/kotlin/fixtures/kotlin-app-variants/Nav.kt +47 -0
  96. package/kotlin/fixtures/kotlin-app-variants/Nested.kt +64 -0
  97. package/kotlin/fixtures/kotlin-app-variants/Pan.kt +38 -0
  98. package/kotlin/fixtures/kotlin-app-variants/Pickers.kt +44 -0
  99. package/kotlin/fixtures/kotlin-app-variants/Press.kt +43 -0
  100. package/kotlin/fixtures/kotlin-app-variants/Transition.kt +69 -0
  101. package/kotlin/fixtures/kotlin-app-variants/ViewConfig.kt +22 -0
  102. package/kotlin/toolchain.json +30 -2
  103. package/package.json +8 -1
  104. package/skills/contrast/SKILL.md +2 -1
  105. package/skills/peach-debug/SKILL.md +2 -2
  106. package/skills/peach-test/SKILL.md +3 -3
  107. package/src/expo-swift-host.ts +13 -4
  108. package/src/host/bridge-host.ts +3 -3
  109. package/src/kotlin-app.ts +35 -16
  110. package/src/kotlin-host.ts +1 -0
  111. package/src/metro-plugin.ts +24 -0
  112. package/src/native-firebase-config.ts +202 -21
  113. package/src/native-firebase-metadata.ts +114 -0
  114. package/src/swift-host.ts +30 -15
  115. package/src/vite-plugin.ts +46 -2
  116. package/kotlin/AppSources/androidx/activity/Cancellable.kt +0 -5
@@ -1,4 +1,4 @@
1
- // the native installer reads Expo's googleServicesFile before Firebase starts.
1
+ // read native default initialization inputs before the guest Firebase SDK starts.
2
2
  // expose only FirebaseOptions, never a partner-specific extra configuration.
3
3
  import fs from 'node:fs'
4
4
  import { createRequire } from 'node:module'
@@ -6,29 +6,210 @@ import path from 'node:path'
6
6
  import { isRecord } from './is-record'
7
7
  import { parsePlistSource } from './plist'
8
8
 
9
- export function readNativeFirebaseConfig(projectRoot: string) {
9
+ export function readNativeFirebaseConfig(
10
+ projectRoot: string,
11
+ platforms: readonly string[] = ['ios', 'android'],
12
+ ) {
10
13
  const result: Record<string, unknown> = {}
14
+ let config: Record<string, unknown> = {}
15
+ const appJsonPath = path.join(projectRoot, 'app.json')
16
+ const appJson: unknown = fs.existsSync(appJsonPath)
17
+ ? JSON.parse(fs.readFileSync(appJsonPath, 'utf8'))
18
+ : null
19
+ const staticExpo =
20
+ isRecord(appJson) &&
21
+ (isRecord(appJson.expo) ||
22
+ [appJson.ios, appJson.android].some(
23
+ (value) => isRecord(value) && typeof value.googleServicesFile === 'string',
24
+ ))
25
+ const dynamicExpo = [
26
+ 'app.config.js',
27
+ 'app.config.ts',
28
+ 'app.config.cjs',
29
+ 'app.config.mjs',
30
+ ].some((name) => fs.existsSync(path.join(projectRoot, name)))
31
+ if (staticExpo || dynamicExpo) {
32
+ const require = createRequire(path.join(projectRoot, 'package.json'))
33
+ const configModule: unknown = require('@expo/config')
34
+ if (!isRecord(configModule) || typeof configModule.getConfig !== 'function')
35
+ throw new Error('Expo native configuration is unavailable.')
36
+ const resolved: unknown = Reflect.apply(configModule.getConfig, configModule, [
37
+ projectRoot,
38
+ { skipSDKVersionRequirement: true },
39
+ ])
40
+ config = isRecord(resolved) && isRecord(resolved.exp) ? resolved.exp : {}
41
+ }
42
+ const ios = config.ios
11
43
  if (
12
- ![
13
- 'app.json',
14
- 'app.config.js',
15
- 'app.config.ts',
16
- 'app.config.cjs',
17
- 'app.config.mjs',
18
- ].some((name) => fs.existsSync(path.join(projectRoot, name)))
19
- )
20
- return result
21
- const require = createRequire(path.join(projectRoot, 'package.json'))
22
- const configModule: unknown = require('@expo/config')
23
- if (!isRecord(configModule) || typeof configModule.getConfig !== 'function') {
24
- throw new Error('Expo native configuration is unavailable.')
44
+ platforms.includes('ios') &&
45
+ (!isRecord(ios) || typeof ios.googleServicesFile !== 'string')
46
+ ) {
47
+ const iosRoot = path.join(projectRoot, 'ios')
48
+ if (fs.existsSync(iosRoot)) {
49
+ const projects = fs
50
+ .readdirSync(iosRoot, { withFileTypes: true })
51
+ .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
52
+ if (projects.length > 1)
53
+ throw new Error('native Firebase configuration requires a selected Xcode project')
54
+ if (projects.length === 1) {
55
+ const require = createRequire(path.join(projectRoot, 'package.json'))
56
+ const xcode: unknown = require('xcode')
57
+ if (!isRecord(xcode) || typeof xcode.project !== 'function')
58
+ throw new Error('native Xcode project parser is unavailable')
59
+ const project: unknown = Reflect.apply(xcode.project, xcode, [
60
+ path.join(iosRoot, projects[0].name, 'project.pbxproj'),
61
+ ])
62
+ if (!isRecord(project) || typeof project.parseSync !== 'function')
63
+ throw new Error('native Xcode project is malformed')
64
+ Reflect.apply(project.parseSync, project, [])
65
+ const hash = project.hash
66
+ const native = isRecord(hash) ? hash.project : null
67
+ const objects = isRecord(native) ? native.objects : null
68
+ if (!isRecord(objects)) throw new Error('native Xcode objects are malformed')
69
+ const unquote = (value: unknown) =>
70
+ typeof value === 'string' ? value.replace(/^"|"$/g, '') : ''
71
+ const targets = isRecord(objects.PBXNativeTarget)
72
+ ? Object.values(objects.PBXNativeTarget).filter(
73
+ (target) =>
74
+ isRecord(target) &&
75
+ unquote(target.productType) === 'com.apple.product-type.application',
76
+ )
77
+ : []
78
+ if (targets.length > 1)
79
+ throw new Error('native Firebase configuration requires a selected app target')
80
+ const target = targets[0]
81
+ if (isRecord(target) && Array.isArray(target.buildPhases)) {
82
+ const buildPhases: readonly unknown[] = target.buildPhases
83
+ const filePaths = new Map<string, Set<string>>()
84
+ const groups = isRecord(objects.PBXGroup) ? objects.PBXGroup : {}
85
+ const references = isRecord(objects.PBXFileReference)
86
+ ? objects.PBXFileReference
87
+ : {}
88
+ const nativeProjects = isRecord(objects.PBXProject)
89
+ ? Object.values(objects.PBXProject)
90
+ : []
91
+ const roots = nativeProjects.filter(isRecord).map((value) => value.mainGroup)
92
+ const pending = roots
93
+ .filter((id): id is string => typeof id === 'string')
94
+ .map((id) => ({ id, base: iosRoot }))
95
+ const visited = new Map<string, string>()
96
+ while (pending.length) {
97
+ const item = pending.pop()
98
+ if (!item) break
99
+ const previous = visited.get(item.id)
100
+ if (previous !== undefined) {
101
+ if (previous !== item.base)
102
+ throw new Error('native Xcode group has ambiguous parents')
103
+ continue
104
+ }
105
+ visited.set(item.id, item.base)
106
+ const group = groups[item.id]
107
+ if (!isRecord(group) || !Array.isArray(group.children)) continue
108
+ const sourceTree = unquote(group.sourceTree)
109
+ if (sourceTree && sourceTree !== '<group>' && sourceTree !== 'SOURCE_ROOT')
110
+ continue
111
+ const directory = path.resolve(
112
+ sourceTree === 'SOURCE_ROOT' ? iosRoot : item.base,
113
+ unquote(group.path),
114
+ )
115
+ for (const child of group.children) {
116
+ if (!isRecord(child) || typeof child.value !== 'string') continue
117
+ if (isRecord(groups[child.value])) {
118
+ pending.push({ id: child.value, base: directory })
119
+ continue
120
+ }
121
+ const file = references[child.value]
122
+ if (!isRecord(file)) continue
123
+ const tree = unquote(file.sourceTree)
124
+ if (tree !== '<group>' && tree !== 'SOURCE_ROOT') continue
125
+ const filePath = path.resolve(
126
+ tree === 'SOURCE_ROOT' ? iosRoot : directory,
127
+ unquote(file.path),
128
+ )
129
+ const values = filePaths.get(child.value) ?? new Set<string>()
130
+ values.add(filePath)
131
+ filePaths.set(child.value, values)
132
+ }
133
+ }
134
+ const buildFiles = isRecord(objects.PBXBuildFile) ? objects.PBXBuildFile : {}
135
+ const filesInPhase = (section: unknown, name: RegExp): string[] => {
136
+ if (!isRecord(section)) return []
137
+ const paths: string[] = []
138
+ for (const phase of buildPhases) {
139
+ if (!isRecord(phase) || typeof phase.value !== 'string') continue
140
+ const value = section[phase.value]
141
+ if (!isRecord(value) || !Array.isArray(value.files)) continue
142
+ for (const entry of value.files) {
143
+ if (!isRecord(entry) || typeof entry.value !== 'string') continue
144
+ const file = buildFiles[entry.value]
145
+ if (!isRecord(file) || typeof file.fileRef !== 'string') continue
146
+ const reference = references[file.fileRef]
147
+ if (
148
+ !isRecord(reference) ||
149
+ !name.test(path.basename(unquote(reference.path)))
150
+ )
151
+ continue
152
+ const candidates = filePaths.get(file.fileRef)
153
+ if (!candidates || candidates.size !== 1)
154
+ throw new Error('native Firebase build resource path is unresolved')
155
+ paths.push(...candidates)
156
+ }
157
+ }
158
+ return paths
159
+ }
160
+ const delegates = filesInPhase(
161
+ objects.PBXSourcesBuildPhase,
162
+ /^AppDelegate\.(swift|mm|m)$/,
163
+ )
164
+ const defaultConfigured = delegates.some((file) => {
165
+ const source = fs
166
+ .readFileSync(file, 'utf8')
167
+ .replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"/g, (token) =>
168
+ token.startsWith('"') ? '""' : token.replace(/[^\n]/g, ' '),
169
+ )
170
+ const launch =
171
+ /(?:\bfunc\s+application\s*\([\s\S]*?didFinishLaunchingWithOptions[\s\S]*?\)\s*->\s*Bool|^-\s*\(BOOL\)\s*application\s*:[\s\S]*?didFinishLaunchingWithOptions[^\{]*)\s*\{/m.exec(
172
+ source,
173
+ )
174
+ if (!launch) return false
175
+ const body = source.slice(launch.index + launch[0].length)
176
+ const configure =
177
+ /^\s*(?:FirebaseApp\s*\.\s*configure\s*\(\s*\)|\[FIRApp\s+configure\])/m.exec(
178
+ body,
179
+ )
180
+ if (!configure) return false
181
+ const before = body.slice(0, configure.index)
182
+ let depth = 1
183
+ for (const token of before) {
184
+ if (token === '{') depth += 1
185
+ if (token === '}') depth -= 1
186
+ if (depth === 0) return false
187
+ }
188
+ if (
189
+ depth !== 1 ||
190
+ /\b(?:return|throw)\b|^\s*#(?:if|elseif|else)/m.test(before)
191
+ )
192
+ throw new Error(
193
+ 'native Firebase initialization must be unconditional in the app launch method',
194
+ )
195
+ return true
196
+ })
197
+ if (defaultConfigured) {
198
+ const resources = filesInPhase(
199
+ objects.PBXResourcesBuildPhase,
200
+ /^GoogleService-Info\.plist$/,
201
+ )
202
+ if (resources.length !== 1)
203
+ throw new Error(
204
+ 'native Firebase default initialization requires one GoogleService-Info.plist build resource',
205
+ )
206
+ config.ios = { googleServicesFile: path.relative(projectRoot, resources[0]) }
207
+ }
208
+ }
209
+ }
210
+ }
25
211
  }
26
- const resolved: unknown = Reflect.apply(configModule.getConfig, configModule, [
27
- projectRoot,
28
- { skipSDKVersionRequirement: true },
29
- ])
30
- const config = isRecord(resolved) && isRecord(resolved.exp) ? resolved.exp : {}
31
- for (const platform of ['ios', 'android']) {
212
+ for (const platform of platforms) {
32
213
  const native = config[platform]
33
214
  if (!isRecord(native) || typeof native.googleServicesFile !== 'string') continue
34
215
  try {
@@ -0,0 +1,114 @@
1
+ // native Firebase options travel before the Metro module identity footer.
2
+ // the guest SDK snapshots these constants before its first getApp call.
3
+ export interface NativeFirebaseOptions {
4
+ apiKey: string
5
+ appId: string
6
+ projectId: string
7
+ databaseURL?: string
8
+ gaTrackingId?: string
9
+ storageBucket?: string
10
+ messagingSenderId?: string
11
+ clientId?: string
12
+ androidClientID?: string
13
+ deepLinkUrlScheme?: string
14
+ }
15
+
16
+ export type NativeFirebaseConfig = Partial<
17
+ Record<'ios' | 'android', NativeFirebaseOptions>
18
+ >
19
+
20
+ const PREFIX = 'globalThis.__peachNativeFirebaseConfig='
21
+ const MODULE_PREFIX = 'globalThis.__sootsimModulePaths='
22
+ const PLATFORMS = ['ios', 'android'] as const
23
+
24
+ export function readNativeFirebaseMetadata(value: unknown): NativeFirebaseConfig | null {
25
+ if (value === null || value === undefined) return null
26
+ if (typeof value !== 'object' || Array.isArray(value))
27
+ throw new Error('native Firebase configuration is malformed')
28
+ if (Object.keys(value).some((key) => !PLATFORMS.some((name) => name === key)))
29
+ throw new Error('native Firebase configuration has an invalid platform')
30
+ const result: NativeFirebaseConfig = {}
31
+ for (const platform of PLATFORMS) {
32
+ const options: unknown = Reflect.get(value, platform)
33
+ if (options === undefined) continue
34
+ if (!options || typeof options !== 'object' || Array.isArray(options))
35
+ throw new Error('native Firebase options are malformed')
36
+ const error: unknown = Reflect.get(options, 'error')
37
+ if (typeof error === 'string')
38
+ throw new Error(`native Firebase configuration: ${error}`)
39
+ const apiKey: unknown = Reflect.get(options, 'apiKey')
40
+ const appId: unknown = Reflect.get(options, 'appId')
41
+ const projectId: unknown = Reflect.get(options, 'projectId')
42
+ if (
43
+ typeof apiKey !== 'string' ||
44
+ !apiKey ||
45
+ typeof appId !== 'string' ||
46
+ !appId ||
47
+ typeof projectId !== 'string' ||
48
+ !projectId
49
+ )
50
+ throw new Error('native Firebase options require apiKey, appId and projectId')
51
+ const native: NativeFirebaseOptions = { apiKey, appId, projectId }
52
+ for (const key of [
53
+ 'databaseURL',
54
+ 'gaTrackingId',
55
+ 'storageBucket',
56
+ 'messagingSenderId',
57
+ 'clientId',
58
+ 'androidClientID',
59
+ 'deepLinkUrlScheme',
60
+ ] as const) {
61
+ const field: unknown = Reflect.get(options, key)
62
+ if (field === undefined) continue
63
+ if (typeof field !== 'string')
64
+ throw new Error(`native Firebase option ${key} is malformed`)
65
+ native[key] = field
66
+ }
67
+ result[platform] = native
68
+ }
69
+ return result
70
+ }
71
+
72
+ export function createNativeFirebaseFooter(config: NativeFirebaseConfig): string {
73
+ const json = JSON.stringify(config)
74
+ .replace(/\u2028/g, '\\u2028')
75
+ .replace(/\u2029/g, '\\u2029')
76
+ .replace(/<\//g, '<\\/')
77
+ return `\n${PREFIX}${json};`
78
+ }
79
+
80
+ export function readNativeFirebaseFooter(source: string): NativeFirebaseConfig | null {
81
+ const start = source.lastIndexOf(`\n${PREFIX}`)
82
+ if (start < 0) return null
83
+ const lineEnd = source.indexOf('\n', start + 1)
84
+ const line = source.slice(start + 1, lineEnd < 0 ? undefined : lineEnd)
85
+ if (!line.endsWith(';')) throw new Error('native Firebase footer is truncated')
86
+ const parsed: unknown = JSON.parse(line.slice(PREFIX.length, -1))
87
+ const config = readNativeFirebaseMetadata(parsed)
88
+ if (config === null) throw new Error('native Firebase footer has no configuration')
89
+ return config
90
+ }
91
+
92
+ export function detachNativeFirebaseFooter(source: string): {
93
+ source: string
94
+ config: NativeFirebaseConfig | null
95
+ } {
96
+ const config = readNativeFirebaseFooter(source)
97
+ if (config === null) return { source, config }
98
+ const start = source.lastIndexOf(`\n${PREFIX}`)
99
+ const lineEnd = source.indexOf('\n', start + 1)
100
+ const end = lineEnd < 0 ? source.length : lineEnd
101
+ return { source: source.slice(0, start) + source.slice(end), config }
102
+ }
103
+
104
+ export function insertNativeFirebaseFooter(
105
+ source: string,
106
+ config: NativeFirebaseConfig,
107
+ ): string {
108
+ const detached = detachNativeFirebaseFooter(source).source
109
+ const start = detached.lastIndexOf(`\n${MODULE_PREFIX}`)
110
+ const footer = createNativeFirebaseFooter(config)
111
+ return start < 0
112
+ ? detached + footer
113
+ : detached.slice(0, start) + footer + detached.slice(start)
114
+ }
package/src/swift-host.ts CHANGED
@@ -600,16 +600,11 @@ async function compile(
600
600
  stamp,
601
601
  package: content,
602
602
  }
603
- const toolchain: unknown = JSON.parse(
604
- fs.readFileSync(path.join(moduleRoot, 'toolchain.json'), 'utf8'),
605
- )
606
- const release =
607
- toolchain && typeof toolchain === 'object'
608
- ? Reflect.get(toolchain, 'release')
609
- : undefined
610
- if (typeof release !== 'string') {
611
- throw new Error(`swift toolchain manifest at ${moduleRoot} has no release`)
612
- }
603
+ const toolchain = localSwiftToolchain(moduleRoot)
604
+ if (!toolchain)
605
+ throw new Error(
606
+ `the Swift toolchain ${moduleRoot}/toolchain.json names is not installed under ~/Library/Developer/Toolchains`,
607
+ )
613
608
  const artifact = await compileSwiftProjectUnit(snapshotCompileInput(snapshot, entry), {
614
609
  // the compile rewrites Sources/App in its root before swiftpm builds, and
615
610
  // its lock orders one process only: two vites on one root (a lane's dev
@@ -627,15 +622,35 @@ async function compile(
627
622
  .slice(0, 16),
628
623
  ),
629
624
  moduleRoot,
630
- release,
631
- toolchainBin: path.join(
632
- os.homedir(),
633
- `Library/Developer/Toolchains/${release}.xctoolchain/usr/bin`,
634
- ),
625
+ ...toolchain,
635
626
  })
636
627
  return { ...artifact, stamp, package: content }
637
628
  }
638
629
 
630
+ // the release a SwiftUI module checkout builds with and its installed
631
+ // toolchain; null when the checkout or the toolchain is missing.
632
+ export function localSwiftToolchain(
633
+ moduleRoot: string,
634
+ ): { release: string; toolchainBin: string } | null {
635
+ const manifest = path.join(moduleRoot, 'toolchain.json')
636
+ if (!fs.existsSync(manifest)) return null
637
+ const toolchain: unknown = JSON.parse(fs.readFileSync(manifest, 'utf8'))
638
+ const release =
639
+ toolchain && typeof toolchain === 'object'
640
+ ? Reflect.get(toolchain, 'release')
641
+ : undefined
642
+ if (typeof release !== 'string') {
643
+ throw new Error(`swift toolchain manifest at ${moduleRoot} has no release`)
644
+ }
645
+ const toolchainBin = path.join(
646
+ os.homedir(),
647
+ `Library/Developer/Toolchains/${release}.xctoolchain/usr/bin`,
648
+ )
649
+ return fs.existsSync(path.join(toolchainBin, 'swift'))
650
+ ? { release, toolchainBin }
651
+ : null
652
+ }
653
+
639
654
  // an npm-installed peachsim ships neither the SwiftUI module nor a toolchain,
640
655
  // so the package compiles where a Contrast project's does: Contrast's compile
641
656
  // route, authorized by `peach login` or PEACH_API_KEY. the service renders the
@@ -33,6 +33,7 @@ import {
33
33
  } from '../../peach-compat/src/stub-manifest.ts'
34
34
  import { isExpoHostSocketModule } from '../../peach-engine/src/guest-module-overrides.ts'
35
35
  import { resolveSootsimBridgePort } from './bridge-constants.ts'
36
+ import { isRecord } from './is-record.ts'
36
37
  import {
37
38
  SHELL_BRIDGE_IDENTITY_PATH,
38
39
  type ShellBridgeIdentity,
@@ -2023,17 +2024,39 @@ async function getDomBabelCore() {
2023
2024
  return domBabelCorePromise
2024
2025
  }
2025
2026
 
2027
+ export const PEACH_DOM_SOURCE_MANIFEST = 'peach-dom-sources.json'
2028
+
2026
2029
  export function expoDomComponentsPlugin(
2027
2030
  options: {
2028
2031
  appDir?: string
2029
2032
  projectRoot?: string
2033
+ sourceManifests?: string[]
2030
2034
  } = {},
2031
2035
  ): Plugin {
2032
2036
  let isProduction = false
2033
2037
  const appDir = options.appDir ? path.resolve(options.appDir) : ''
2034
- const projectRoot = options.projectRoot ?? appDir ?? workspaceRoot
2038
+ const projectRoot = options.projectRoot ?? (appDir || workspaceRoot)
2035
2039
  const domHashMap = new Map<string, string>()
2036
2040
  const domWebModuleIds = new Set<string>()
2041
+ const deliveredSources = new Map<string, string>()
2042
+ let sourcesLoaded = false
2043
+
2044
+ function loadDeliveredSources() {
2045
+ if (sourcesLoaded) return
2046
+ for (const manifest of options.sourceManifests ?? []) {
2047
+ const sources: unknown = JSON.parse(fs.readFileSync(manifest, 'utf8'))
2048
+ if (!isRecord(sources)) throw new Error(`invalid DOM source manifest: ${manifest}`)
2049
+ for (const [reference, relativePath] of Object.entries(sources)) {
2050
+ if (typeof relativePath !== 'string' || path.isAbsolute(relativePath)) {
2051
+ throw new Error(`invalid DOM source path: ${reference}`)
2052
+ }
2053
+ const localUrl = pathToFileURL(path.resolve(projectRoot, relativePath)).href
2054
+ deliveredSources.set(reference, localUrl)
2055
+ domHashMap.set(crypto.createHash('md5').update(reference).digest('hex'), localUrl)
2056
+ }
2057
+ }
2058
+ sourcesLoaded = true
2059
+ }
2037
2060
 
2038
2061
  // specifiers that must stay runtime requires served by the page's
2039
2062
  // window.require shim instead of becoming static imports:
@@ -2133,6 +2156,24 @@ export function expoDomComponentsPlugin(
2133
2156
  (config.mode === 'production' || config.isProduction) &&
2134
2157
  process.env.SOOTSIM_TENANT_BUILD_MODE !== 'development'
2135
2158
  },
2159
+ generateBundle() {
2160
+ const sources = Object.fromEntries(
2161
+ [...domHashMap.values()]
2162
+ .sort()
2163
+ .map((reference) => [
2164
+ reference,
2165
+ path
2166
+ .relative(projectRoot, fileURLToPath(reference))
2167
+ .split(path.sep)
2168
+ .join('/'),
2169
+ ]),
2170
+ )
2171
+ this.emitFile({
2172
+ type: 'asset',
2173
+ fileName: PEACH_DOM_SOURCE_MANIFEST,
2174
+ source: JSON.stringify(sources),
2175
+ })
2176
+ },
2136
2177
  configureServer(server) {
2137
2178
  server.middlewares.use((req, res, next) => {
2138
2179
  if (!req.url) return next()
@@ -2142,6 +2183,7 @@ export function expoDomComponentsPlugin(
2142
2183
  rawUrl.startsWith('/www.bundle') ||
2143
2184
  rawUrl.startsWith('www.bundle')
2144
2185
  if (!isDomRoute) return next()
2186
+ loadDeliveredSources()
2145
2187
 
2146
2188
  const url = new URL(
2147
2189
  rawUrl.startsWith('/') ? rawUrl : '/' + rawUrl,
@@ -2499,7 +2541,9 @@ export default NoopMetroHMRClient;
2499
2541
  if (id.startsWith(`${virtualIdPrefix}_expo_dom_entry`)) {
2500
2542
  const query = id.slice(`${virtualIdPrefix}_expo_dom_entry`.length)
2501
2543
  const params = new URLSearchParams(query)
2502
- const fileUrl = params.get('file')
2544
+ loadDeliveredSources()
2545
+ const reference = params.get('file')
2546
+ const fileUrl = reference && (deliveredSources.get(reference) ?? reference)
2503
2547
  if (!fileUrl) {
2504
2548
  throw new Error(`Missing file query param in DOM component entry: ${id}`)
2505
2549
  }
@@ -1,5 +0,0 @@
1
- package androidx.activity
2
-
3
- internal interface Cancellable {
4
- fun cancel()
5
- }