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.
- package/LICENSE +1 -1
- package/README.md +45 -6
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +39 -18
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +248 -156
- package/cli/cloud-client.ts +68 -57
- package/cli/command-registry.ts +1 -0
- package/cli/commands/box/search-command.ts +3 -2
- package/cli/commands/box.ts +10 -29
- package/cli/commands/control.ts +391 -92
- package/cli/commands/doctor.ts +17 -184
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +71 -22
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +26 -3
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +105 -67
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/platform.ts +71 -36
- package/cli/commands/record.ts +21 -1
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +5 -0
- package/cli/commands/upload.ts +51 -8
- package/cli/desktop-companion.ts +102 -17
- package/cli/dev-build-freshness.ts +37 -2
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +44 -8
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +53 -0
- package/cli/maestro-compare.ts +108 -12
- package/cli/main.ts +2 -1
- package/cli/outbound-endpoints.ts +35 -6
- package/cli/parent-pid.ts +0 -9
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/remote-simulators.ts +112 -0
- package/cli/remoteSimulatorContract.ts +21 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/expectations.ts +5 -1
- package/detox/index.ts +241 -181
- package/detox/motion-change.cjs +73 -0
- package/detox/motion-change.d.cts +21 -0
- package/detox/navigation-lifecycle.ts +22 -4
- package/dist-lib/agent-daemon-client.cjs +3 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +3 -2
- 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 +13 -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 +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +10480 -7962
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +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 +3 -2
- package/dist-lib/cloud.mjs +3 -2
- package/dist-lib/config.cjs +10 -6
- package/dist-lib/config.mjs +10 -6
- package/dist-lib/detox/index.cjs +1854 -966
- package/dist-lib/dev-bundle-resolution.cjs +64 -24
- package/dist-lib/expo-swift-bundle.cjs +8715 -6271
- package/dist-lib/expo-swift-metadata.cjs +7 -4
- package/dist-lib/expo-swift-metadata.mjs +7 -4
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +10135 -7060
- 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 +1843 -408
- 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 +1 -1
- package/dist-lib/menu.cjs +3 -2
- package/dist-lib/menu.mjs +3 -2
- 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 +5 -4
- package/dist-lib/metro-production-bundle.mjs +5 -4
- package/dist-lib/metro.cjs +1182 -418
- package/dist-lib/native-app-identity-project.cjs +782 -23
- package/dist-lib/native-app-identity-project.mjs +781 -23
- package/dist-lib/native-app-identity.cjs +276 -9
- package/dist-lib/native-app-identity.mjs +273 -8
- package/dist-lib/native-firebase-metadata.cjs +5 -4
- package/dist-lib/native-firebase-metadata.mjs +5 -4
- 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 +3 -2
- package/dist-lib/react-native-host-modules.cjs +8 -1
- package/dist-lib/react-native-host-modules.mjs +8 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
- package/dist-lib/sdk.cjs +115 -34
- package/dist-lib/sdk.mjs +115 -33
- package/dist-lib/skills.cjs +15274 -9904
- package/dist-lib/swift-compile.cjs +1165 -173
- package/dist-lib/swift-compile.mjs +1212 -203
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +7 -1
- package/dist-lib/swift-submit.mjs +5 -1
- package/dist-lib/swift-toolchain.cjs +133 -18
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +2648 -862
- package/dist-lib/vite.cjs +70 -14
- package/dist-lib/worklet-executable.cjs +25 -2
- package/dist-lib/worklet-executable.mjs +25 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +2 -2
- package/scripts/dev-server-scanner.ts +35 -14
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/admitted-mac-build.ts +34 -8
- package/src/android-app-identity.ts +146 -0
- package/src/android-autolinking.ts +75 -0
- package/src/android-string-value.ts +36 -0
- package/src/android-strings.ts +5 -36
- package/src/androidAutolinkingContract.ts +40 -0
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/bundleGlobalFooter.ts +26 -7
- package/src/codex-client.ts +3 -2
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +60 -26
- package/src/expo-kotlin-host.ts +6 -36
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-bundle.ts +17 -12
- package/src/expo-swift-host.ts +640 -192
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +19 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +12 -57
- package/src/host/staged-host-files.ts +29 -2
- package/src/installed-package-versions.ts +54 -14
- package/src/launchMetadataContract.ts +24 -0
- package/src/metro-plugin.ts +37 -42
- package/src/native-app-identity-project.ts +142 -5
- package/src/native-app-identity.ts +176 -11
- package/src/native-firebase-config.ts +209 -13
- package/src/nitro-compile.ts +2 -5
- package/src/nitro-source.ts +147 -63
- package/src/process-group.ts +114 -61
- package/src/process-output.ts +0 -9
- package/src/public-brand.ts +1 -0
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +7 -0
- package/src/sim-client.ts +6 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +1007 -108
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +40 -12
- package/src/swift-distribution.ts +61 -9
- package/src/swift-host.ts +149 -56
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +38 -3
- package/src/swift-package-manifest.ts +21 -6
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +119 -60
- package/src/swift-standin-emit.ts +57 -9
- package/src/swift-standins.ts +185 -25
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +20 -20
- package/src/worklet-executable.ts +30 -3
- package/src/worklets-babel.ts +35 -14
- 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
|
+
}
|
package/src/android-strings.ts
CHANGED
|
@@ -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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
package/src/bridge-constants.ts
CHANGED
|
@@ -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
|
package/src/bridge-contract.ts
CHANGED
|
@@ -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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
407
|
-
probe:
|
|
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
|
|
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,
|
package/src/codex-client.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 [
|