peachsim 0.1.668 → 0.1.670
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.
- package/cli/commands/control.ts +8 -5
- package/cli/commands/inspect/core.ts +7 -5
- package/cli/commands/inspect/wait-selector.ts +2 -11
- package/cli/commands/inspect.ts +6 -59
- package/cli/commands/kotlin.ts +6 -4
- package/cli/commands/skills.ts +28 -7
- package/cli/commands/upgrade.ts +18 -3
- package/cli/main.ts +21 -0
- package/cli/output-redaction.ts +182 -0
- package/cli/parse-args.ts +1 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +29 -19
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +260 -17
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +2 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +259 -16
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +115 -0
- package/dist-lib/native-firebase-metadata.mjs +86 -0
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2 -2
- package/dist-lib/sdk.mjs +2 -2
- package/dist-lib/skills.cjs +479 -204
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +21 -13
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/androidx/activity/ActivityFlags.kt +9 -0
- package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +25 -4
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +15 -4
- package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +12 -8
- package/kotlin/AppSources/androidx/activity/ExperimentalActivityApi.kt +6 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +50 -9
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +67 -89
- package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -24
- package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerCompat.kt +38 -0
- package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerDispatcherCompat.kt +27 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +5 -5
- package/kotlin/ComposeSources/PeachComposeHost.kt +31 -7
- package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +1 -1
- package/kotlin/fixtures/kotlin-app-variants/Browse.kt +73 -0
- package/kotlin/fixtures/kotlin-app-variants/Focus.kt +62 -0
- package/kotlin/fixtures/kotlin-app-variants/Lists.kt +57 -0
- package/kotlin/fixtures/kotlin-app-variants/Nav.kt +47 -0
- package/kotlin/fixtures/kotlin-app-variants/Nested.kt +64 -0
- package/kotlin/fixtures/kotlin-app-variants/Pan.kt +38 -0
- package/kotlin/fixtures/kotlin-app-variants/Pickers.kt +44 -0
- package/kotlin/fixtures/kotlin-app-variants/Press.kt +43 -0
- package/kotlin/fixtures/kotlin-app-variants/TextWidth.kt +45 -0
- package/kotlin/fixtures/kotlin-app-variants/Transition.kt +69 -0
- package/kotlin/fixtures/kotlin-app-variants/ViewConfig.kt +22 -0
- package/kotlin/toolchain.json +30 -2
- package/package.json +8 -1
- package/skills/contrast/SKILL.md +2 -1
- package/skills/peach-debug/SKILL.md +2 -2
- package/skills/peach-test/SKILL.md +3 -3
- package/src/expo-swift-host.ts +13 -4
- package/src/kotlin-app.ts +35 -16
- package/src/kotlin-host.ts +1 -0
- package/src/metro-plugin.ts +24 -0
- package/src/native-firebase-config.ts +202 -21
- package/src/native-firebase-metadata.ts +114 -0
- package/src/swift-host.ts +30 -15
- package/src/vite-plugin.ts +46 -2
- package/kotlin/AppSources/androidx/activity/Cancellable.kt +0 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
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(
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
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
|
package/src/vite-plugin.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|