peachsim 0.1.656 → 0.1.658
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/README.md +22 -3
- package/cli/app-config.ts +34 -15
- package/cli/app-project.ts +6 -6
- package/cli/bridge-flow-runner.ts +10 -23
- package/cli/commands/compat.ts +234 -10
- package/cli/commands/control.ts +20 -20
- package/cli/commands/do-chain.ts +5 -1
- package/cli/commands/flow.ts +3 -3
- package/cli/commands/inspect/actions.ts +33 -76
- package/cli/commands/inspect/resolve-target.ts +67 -2
- package/cli/commands/inspect.ts +16 -1
- package/cli/drivers/playwright-provisioning.ts +95 -0
- package/cli/ws-bridge.ts +7 -7
- package/detox/config-loader.ts +1 -1
- 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 +9 -1
- package/dist-lib/bridge-contract-input.mjs +9 -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 +27 -26
- 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 +90 -47
- 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 +51 -8
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +317 -70
- 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 +8 -4
- 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 +300 -29
- package/dist-lib/native-app-identity.cjs +58 -25
- package/dist-lib/native-app-identity.mjs +57 -25
- package/dist-lib/plist.cjs +64 -0
- package/dist-lib/plist.mjs +29 -0
- 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 +14 -16
- package/dist-lib/sdk.cjs +36 -49
- package/dist-lib/sdk.mjs +36 -49
- package/dist-lib/skills.cjs +341 -198
- 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 +1 -1
- package/dist-lib/vite.cjs +51 -8
- package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
- package/kotlin/AppSources/android/content/Context.kt +9 -2
- package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
- package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
- package/kotlin/AppSources/android/graphics/Color.kt +13 -0
- package/kotlin/AppSources/android/view/View.kt +2 -0
- package/kotlin/AppSources/android/view/Window.kt +99 -8
- package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
- package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
- package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
- package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
- package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
- package/package.json +8 -1
- package/scripts/dev-server-scanner.ts +3 -3
- package/scripts/optional-demo-registry.ts +1 -1
- package/skills/peach-setup/SKILL.md +27 -0
- package/src/android-theme.ts +167 -0
- package/src/android-xml.ts +70 -0
- package/src/bridge-contract-input.ts +10 -1
- package/src/bridge-contract.ts +7 -1
- package/src/config.ts +34 -26
- package/src/generated/android-framework-themes.json +6303 -0
- package/src/host/replacement-module-handler.ts +33 -0
- package/src/index.ts +1 -0
- package/src/kotlin-app.ts +75 -61
- package/src/kotlin-host.ts +7 -3
- package/src/metro-plugin.ts +46 -3
- package/src/native-app-identity.ts +75 -26
- package/src/sim-client.ts +2 -4
package/README.md
CHANGED
|
@@ -243,7 +243,22 @@ Windows use Playwright's Chromium headless shell instead: the same build and
|
|
|
243
243
|
software renderer without the desktop browser's printing and service discovery
|
|
244
244
|
libraries (`libcups2`, `libavahi-client3`). Headed launches and every launch on
|
|
245
245
|
a Mac use full Chromium. When the needed browser is not installed, the CLI runs
|
|
246
|
-
the resolved Playwright core's absolute installer entry for just that browser.
|
|
246
|
+
the resolved Playwright core's absolute installer entry for just that browser.
|
|
247
|
+
On Linux the CLI checks the browser's shared libraries before launching and,
|
|
248
|
+
when any are missing, exits naming them and the exact `apt-get install` line.
|
|
249
|
+
The headless shell needs these Debian 12 packages (Ubuntu 24.04 and Debian 13
|
|
250
|
+
add a `t64` suffix to `libglib2.0-0`, `libatk1.0-0`, `libatk-bridge2.0-0`,
|
|
251
|
+
`libatspi2.0-0` and `libasound2`):
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
apt-get install --no-install-recommends libglib2.0-0 libnspr4 libnss3 \
|
|
255
|
+
libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libdbus-1-3 libx11-6 libxcb1 \
|
|
256
|
+
libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libexpat1 \
|
|
257
|
+
libxkbcommon0 libudev1 libasound2
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Headed launches on Linux use full Chromium, which also needs `libcups2`,
|
|
261
|
+
`libcairo2` and `libpango-1.0-0`. Later opens reuse that sim and
|
|
247
262
|
reload it at the requested target. Use `peach do reload` when the target has not
|
|
248
263
|
changed.
|
|
249
264
|
Reserve `--new` for a genuinely concurrent sim; repeated new browser trees
|
|
@@ -627,8 +642,12 @@ Per-repo configuration is optional. Every installed-runtime `peach open`
|
|
|
627
642
|
serves its selected version from a version-specific localhost origin, so a
|
|
628
643
|
background activation cannot mix an already loaded shell with chunks from the
|
|
629
644
|
next runtime. Without config, each new open selects the active stable version.
|
|
630
|
-
When `
|
|
631
|
-
version without changing the machine default.
|
|
645
|
+
When `peach.config.ts` sets `runtimeVersion`, `peach open` installs that
|
|
646
|
+
version without changing the machine default. The same file replaces an app's own
|
|
647
|
+
native modules (`nativeModules`, `turboModules`) and native views
|
|
648
|
+
(`nativeViews`, keyed by the `requireNativeComponent` name or an Expo view's
|
|
649
|
+
`Module_View` name) with project-local files; the configuration guide in the
|
|
650
|
+
docs lists every slot. App config, native-linked fonts,
|
|
632
651
|
and splash assets all resolve from the project root advertised by the selected
|
|
633
652
|
development server. Bare Metro, which has no Expo manifest project root, uses
|
|
634
653
|
the listening development process's cwd inside that same resolver. The invoking
|
package/cli/app-config.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { isValidRuntimeVersion } from '@contrast/runtime-delivery'
|
|
|
4
4
|
import { createJiti } from 'jiti'
|
|
5
5
|
import { rnxPublicBrand } from '../src/public-brand'
|
|
6
6
|
import { resolveAppProject, type ResolvedAppProject } from './app-project'
|
|
7
|
-
import type {
|
|
7
|
+
import type { PeachConfig } from '../src/config'
|
|
8
8
|
|
|
9
9
|
const require = createRequire(import.meta.url)
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ function isOptionalRecord(value: unknown): boolean {
|
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function
|
|
18
|
+
function isPeachConfig(value: unknown): value is PeachConfig {
|
|
19
19
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
20
20
|
const runtimeVersion = Reflect.get(value, 'runtimeVersion')
|
|
21
21
|
const network = Reflect.get(value, 'network')
|
|
@@ -27,6 +27,7 @@ function isRNXConfig(value: unknown): value is RNXConfig {
|
|
|
27
27
|
isOptionalRecord(Reflect.get(value, 'modules')) &&
|
|
28
28
|
isOptionalRecord(Reflect.get(value, 'turboModules')) &&
|
|
29
29
|
isOptionalRecord(Reflect.get(value, 'nativeModules')) &&
|
|
30
|
+
isOptionalRecord(Reflect.get(value, 'nativeViews')) &&
|
|
30
31
|
isOptionalRecord(Reflect.get(value, 'env')) &&
|
|
31
32
|
isOptionalRecord(network) &&
|
|
32
33
|
isOptionalRecord(Reflect.get(value, 'settings')) &&
|
|
@@ -34,25 +35,43 @@ function isRNXConfig(value: unknown): value is RNXConfig {
|
|
|
34
35
|
)
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// `{ file }` entries are relative to the config file; the engine loads them by
|
|
39
|
+
// absolute path from the runtime's replacement-module endpoint.
|
|
40
|
+
function resolveFileEntries<T>(
|
|
41
|
+
entries: Record<string, T> | undefined,
|
|
42
|
+
configDir: string,
|
|
43
|
+
): Record<string, T> | undefined {
|
|
44
|
+
if (!entries) return entries
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(entries).map(([name, resolution]) => [
|
|
47
|
+
name,
|
|
48
|
+
resolution &&
|
|
49
|
+
typeof resolution === 'object' &&
|
|
50
|
+
'file' in resolution &&
|
|
51
|
+
typeof resolution.file === 'string'
|
|
52
|
+
? { ...resolution, file: path.resolve(configDir, resolution.file) }
|
|
53
|
+
: resolution,
|
|
54
|
+
]),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveConfigReplacementFiles(
|
|
59
|
+
config: PeachConfig,
|
|
60
|
+
configPath: string,
|
|
61
|
+
): PeachConfig {
|
|
39
62
|
const configDir = path.dirname(configPath)
|
|
40
63
|
return {
|
|
41
64
|
...config,
|
|
42
|
-
modules:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
? { ...resolution, file: path.resolve(configDir, resolution.file) }
|
|
47
|
-
: resolution,
|
|
48
|
-
]),
|
|
49
|
-
),
|
|
65
|
+
modules: resolveFileEntries(config.modules, configDir),
|
|
66
|
+
turboModules: resolveFileEntries(config.turboModules, configDir),
|
|
67
|
+
nativeModules: resolveFileEntries(config.nativeModules, configDir),
|
|
68
|
+
nativeViews: resolveFileEntries(config.nativeViews, configDir),
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
export async function loadRNXAppConfig(
|
|
54
73
|
project: ResolvedAppProject | null,
|
|
55
|
-
): Promise<
|
|
74
|
+
): Promise<PeachConfig | undefined> {
|
|
56
75
|
if (!project?.configPath) return undefined
|
|
57
76
|
try {
|
|
58
77
|
const jiti = createJiti(import.meta.url, {
|
|
@@ -64,7 +83,7 @@ export async function loadRNXAppConfig(
|
|
|
64
83
|
},
|
|
65
84
|
})
|
|
66
85
|
const loaded: unknown = await jiti.import(project.configPath, { default: true })
|
|
67
|
-
if (!
|
|
86
|
+
if (!isPeachConfig(loaded)) {
|
|
68
87
|
throw new Error('the default export must be a peach config object')
|
|
69
88
|
}
|
|
70
89
|
return resolveConfigReplacementFiles(loaded, project.configPath)
|
|
@@ -76,7 +95,7 @@ export async function loadRNXAppConfig(
|
|
|
76
95
|
|
|
77
96
|
export async function resolveRNXAppConfig(
|
|
78
97
|
startDir: string = process.cwd(),
|
|
79
|
-
): Promise<{ project: ResolvedAppProject | null; config:
|
|
98
|
+
): Promise<{ project: ResolvedAppProject | null; config: PeachConfig | undefined }> {
|
|
80
99
|
const project = resolveAppProject(startDir)
|
|
81
100
|
return { project, config: await loadRNXAppConfig(project) }
|
|
82
101
|
}
|
package/cli/app-project.ts
CHANGED
|
@@ -9,11 +9,11 @@ export const EXPO_APP_CONFIG_NAMES = [
|
|
|
9
9
|
'app.json',
|
|
10
10
|
] as const
|
|
11
11
|
|
|
12
|
-
export const
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
12
|
+
export const PEACH_APP_CONFIG_NAMES = [
|
|
13
|
+
'peach.config.ts',
|
|
14
|
+
'peach.config.js',
|
|
15
|
+
'peach.config.mjs',
|
|
16
|
+
'peach.config.cjs',
|
|
17
17
|
] as const
|
|
18
18
|
|
|
19
19
|
export interface ResolvedAppProject {
|
|
@@ -36,7 +36,7 @@ export function findExpoAppConfig(dir: string): string | null {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function findRNXAppConfig(dir: string): string | null {
|
|
39
|
-
return firstNamedFile(dir,
|
|
39
|
+
return firstNamedFile(dir, PEACH_APP_CONFIG_NAMES)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function resolveAppProject(projectRoot: string): ResolvedAppProject | null {
|
|
@@ -920,7 +920,6 @@ export class SootSimBridgeFlowRunner {
|
|
|
920
920
|
|
|
921
921
|
private firstLaunchDone = false
|
|
922
922
|
private appStopped = false
|
|
923
|
-
private launchArgumentsActive = false
|
|
924
923
|
private profilingEnabled = false
|
|
925
924
|
private recordingEnabled = false
|
|
926
925
|
private recordingAccessChecked = false
|
|
@@ -2604,22 +2603,6 @@ export class SootSimBridgeFlowRunner {
|
|
|
2604
2603
|
throw new Error('extendedWaitUntil timed out')
|
|
2605
2604
|
}
|
|
2606
2605
|
|
|
2607
|
-
private async reloadGuestApp() {
|
|
2608
|
-
// try the gentle reload first — in-place React re-mount preserves
|
|
2609
|
-
// recording state and avoids the full worker lifecycle tear-down.
|
|
2610
|
-
const reloaded = await this.bridge.send({
|
|
2611
|
-
type: 'call',
|
|
2612
|
-
simId: this.opts.simId,
|
|
2613
|
-
path: 'SootSim.bridges.hotRemount.reloadExternalApp',
|
|
2614
|
-
args: [],
|
|
2615
|
-
})
|
|
2616
|
-
if (reloaded) return
|
|
2617
|
-
|
|
2618
|
-
// fallback for surfaces that don't export the hot-remount bridge: a
|
|
2619
|
-
// hard page reload resets the tenant worker.
|
|
2620
|
-
await this.evaluate('window.location.reload()').catch(() => {})
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
2606
|
private async resetGuestAppData(
|
|
2624
2607
|
launchOptions?: Pick<
|
|
2625
2608
|
SootSimExternalAppLifecycleOptions,
|
|
@@ -2690,6 +2673,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2690
2673
|
}
|
|
2691
2674
|
const wantsClear = !!(opts && typeof opts === 'object' && opts.clearState)
|
|
2692
2675
|
const wantsResetRuntime = readBooleanKey(opts, 'resetRuntime')
|
|
2676
|
+
const wantsForeground = isObjectRecord(opts) && opts.stopApp === false
|
|
2693
2677
|
await this.setLaunchPermissions(isObjectRecord(opts) ? opts.permissions : undefined)
|
|
2694
2678
|
|
|
2695
2679
|
if (!this.firstLaunchDone) {
|
|
@@ -2727,7 +2711,14 @@ export class SootSimBridgeFlowRunner {
|
|
|
2727
2711
|
await this.waitForTree(120000)
|
|
2728
2712
|
await this.waitForTree(30000)
|
|
2729
2713
|
} else {
|
|
2730
|
-
await this.launchShellAppFromHomeIfNeeded(opts)
|
|
2714
|
+
const launched = await this.launchShellAppFromHomeIfNeeded(opts)
|
|
2715
|
+
if (!launched && !wantsForeground) {
|
|
2716
|
+
await this.waitForTree(120000)
|
|
2717
|
+
await this.reloadGuestAppRuntime({
|
|
2718
|
+
launchArguments: null,
|
|
2719
|
+
initialUrl: null,
|
|
2720
|
+
})
|
|
2721
|
+
}
|
|
2731
2722
|
await this.waitForTree(120000)
|
|
2732
2723
|
await this.waitForTree(30000)
|
|
2733
2724
|
}
|
|
@@ -2737,13 +2728,11 @@ export class SootSimBridgeFlowRunner {
|
|
|
2737
2728
|
launchArguments: hasLaunchArguments ? launchArguments : null,
|
|
2738
2729
|
initialUrl: null,
|
|
2739
2730
|
})
|
|
2740
|
-
} else if (hasLaunchArguments || this.
|
|
2731
|
+
} else if (hasLaunchArguments || this.appStopped || !wantsForeground) {
|
|
2741
2732
|
await this.reloadGuestAppRuntime({
|
|
2742
2733
|
launchArguments: hasLaunchArguments ? launchArguments : null,
|
|
2743
2734
|
initialUrl: null,
|
|
2744
2735
|
})
|
|
2745
|
-
} else {
|
|
2746
|
-
await this.reloadGuestApp()
|
|
2747
2736
|
}
|
|
2748
2737
|
await this.waitForTree(120000)
|
|
2749
2738
|
if (this.opts.onAfterLaunch) {
|
|
@@ -2760,7 +2749,6 @@ export class SootSimBridgeFlowRunner {
|
|
|
2760
2749
|
}
|
|
2761
2750
|
|
|
2762
2751
|
this.appStopped = false
|
|
2763
|
-
this.launchArgumentsActive = hasLaunchArguments
|
|
2764
2752
|
|
|
2765
2753
|
if (this.profilingEnabled) {
|
|
2766
2754
|
await this.startProfile()
|
|
@@ -3014,7 +3002,6 @@ export class SootSimBridgeFlowRunner {
|
|
|
3014
3002
|
throw new Error(`openLink failed: ${result?.error || 'no test bridge'}`)
|
|
3015
3003
|
}
|
|
3016
3004
|
this.appStopped = false
|
|
3017
|
-
this.launchArgumentsActive = false
|
|
3018
3005
|
this.firstLaunchDone = true
|
|
3019
3006
|
}
|
|
3020
3007
|
|
package/cli/commands/compat.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
// peach compat — scan dependencies with the shared compatibility engine
|
|
1
|
+
// peach compat — scan dependencies with the shared compatibility engine, and
|
|
2
|
+
// the app's own native modules and views.
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
statSync,
|
|
11
|
+
} from 'node:fs'
|
|
12
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
4
13
|
import {
|
|
5
14
|
COMPAT_RESULT_CAVEAT,
|
|
6
15
|
formatDetailedScanLines,
|
|
@@ -8,8 +17,11 @@ import {
|
|
|
8
17
|
scanDeps,
|
|
9
18
|
type ScanResult,
|
|
10
19
|
} from '@peach/compat/web'
|
|
20
|
+
import { rnxPublicBrand } from '../../src/public-brand'
|
|
21
|
+
import { resolveRNXAppConfig } from '../app-config'
|
|
11
22
|
import {
|
|
12
23
|
findProjectRoot,
|
|
24
|
+
scanRepository,
|
|
13
25
|
scanWorkspace,
|
|
14
26
|
installedManifest,
|
|
15
27
|
readProjectManifest,
|
|
@@ -19,7 +31,7 @@ import type { ProjectManifest } from './projectReportContract'
|
|
|
19
31
|
export async function runCompat(args: string[]): Promise<void> {
|
|
20
32
|
if (args.includes('--help') || args.includes('-h')) {
|
|
21
33
|
console.log(`
|
|
22
|
-
peach compat — check package compatibility
|
|
34
|
+
peach compat — check package compatibility and the app's own native code
|
|
23
35
|
|
|
24
36
|
usage:
|
|
25
37
|
peach compat [package-name]
|
|
@@ -48,13 +60,13 @@ flags:
|
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
if (packageName) {
|
|
51
|
-
report(args, scanOnePackage(appDir, pkg, packageName))
|
|
63
|
+
report(args, scanOnePackage(appDir, pkg, packageName), null)
|
|
52
64
|
return
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
const here = scanDeps(installedManifest(appDir, pkg))
|
|
56
68
|
if (here.packages.length > 0 || appIndex >= 0) {
|
|
57
|
-
report(args, here)
|
|
69
|
+
report(args, here, await scanLocalNative(appDir, pkg))
|
|
58
70
|
return
|
|
59
71
|
}
|
|
60
72
|
|
|
@@ -67,13 +79,13 @@ flags:
|
|
|
67
79
|
const manifest = readProjectManifest(app.dir)
|
|
68
80
|
if (!manifest) return []
|
|
69
81
|
const result = scanDeps(installedManifest(app.dir, manifest))
|
|
70
|
-
return result.packages.length > 0 ? [{ dir: app.dir, result }] : []
|
|
82
|
+
return result.packages.length > 0 ? [{ dir: app.dir, manifest, result }] : []
|
|
71
83
|
})
|
|
72
84
|
|
|
73
85
|
if (elsewhere.length === 1) {
|
|
74
86
|
const only = elsewhere[0]
|
|
75
87
|
console.log(`\n scanning ${relative(appDir, only.dir) || '.'}`)
|
|
76
|
-
report(args, only.result)
|
|
88
|
+
report(args, only.result, await scanLocalNative(resolve(only.dir), only.manifest))
|
|
77
89
|
return
|
|
78
90
|
}
|
|
79
91
|
if (elsewhere.length > 1) {
|
|
@@ -84,16 +96,20 @@ flags:
|
|
|
84
96
|
console.log()
|
|
85
97
|
return
|
|
86
98
|
}
|
|
87
|
-
report(args, here)
|
|
99
|
+
report(args, here, await scanLocalNative(appDir, pkg))
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
function report(args: string[], result: ScanResult): void {
|
|
102
|
+
function report(args: string[], result: ScanResult, local: LocalNative[] | null): void {
|
|
91
103
|
if (args.includes('--json')) {
|
|
92
|
-
console.log(
|
|
104
|
+
console.log(
|
|
105
|
+
JSON.stringify(local ? { ...result, localNative: local } : result, null, 2),
|
|
106
|
+
)
|
|
93
107
|
return
|
|
94
108
|
}
|
|
95
109
|
if (args.includes('--brief')) {
|
|
96
110
|
console.log(` ${formatScanCounts(result)}`)
|
|
111
|
+
if (local?.length)
|
|
112
|
+
console.log(` the app's own native modules and views: ${local.length}`)
|
|
97
113
|
console.log(` ${COMPAT_RESULT_CAVEAT}`)
|
|
98
114
|
return
|
|
99
115
|
}
|
|
@@ -110,9 +126,217 @@ function report(args: string[], result: ScanResult): void {
|
|
|
110
126
|
if (result.packages.length === 0) {
|
|
111
127
|
console.log(` no React Native ecosystem packages detected`)
|
|
112
128
|
}
|
|
129
|
+
if (local?.length) reportLocalNative(local)
|
|
113
130
|
console.log(`\n ${COMPAT_RESULT_CAVEAT}\n`)
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
function reportLocalNative(local: LocalNative[]): void {
|
|
134
|
+
console.log(`\n the app's own native modules and views`)
|
|
135
|
+
console.log(` Peach does not run an app's native code. until peach.config.ts answers`)
|
|
136
|
+
console.log(` for one, a module is an inert stand-in and a view renders empty.\n`)
|
|
137
|
+
const open = local.filter((item) => !item.answered)
|
|
138
|
+
const width = Math.max(...local.map((item) => item.name.length))
|
|
139
|
+
for (const item of local) {
|
|
140
|
+
const where = item.expoModule
|
|
141
|
+
? `${item.path} (Expo module ${item.expoModule}${item.swift ? ', Swift' : ''})`
|
|
142
|
+
: item.path
|
|
143
|
+
console.log(
|
|
144
|
+
` ${item.kind === 'view' ? 'view ' : 'module'} ${item.name.padEnd(width)} ${where}${item.answered ? ' answered in peach.config.ts' : ''}`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
const modules = open.filter((item) => item.kind === 'module')
|
|
148
|
+
const views = open.filter((item) => item.kind === 'view')
|
|
149
|
+
if (open.length) {
|
|
150
|
+
console.log(
|
|
151
|
+
`\n answer ${open.length === local.length ? 'them' : 'the rest'} from peach.config.ts:\n`,
|
|
152
|
+
)
|
|
153
|
+
console.log(` export default defineConfig({`)
|
|
154
|
+
if (modules.length) {
|
|
155
|
+
console.log(` nativeModules: {`)
|
|
156
|
+
for (const item of modules) {
|
|
157
|
+
console.log(` ${item.name}: { file: './peach/${item.name}.ts' },`)
|
|
158
|
+
}
|
|
159
|
+
console.log(` },`)
|
|
160
|
+
}
|
|
161
|
+
if (views.length) {
|
|
162
|
+
console.log(` nativeViews: {`)
|
|
163
|
+
for (const item of views) {
|
|
164
|
+
console.log(` ${item.name}: { file: './peach/${item.name}.tsx' },`)
|
|
165
|
+
}
|
|
166
|
+
console.log(` },`)
|
|
167
|
+
}
|
|
168
|
+
console.log(` })`)
|
|
169
|
+
}
|
|
170
|
+
console.log(`\n guide: ${rnxPublicBrand.origin}/docs/rnx/compat/custom-stubs`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// an app's own native code, found from the names its JavaScript asks the native
|
|
174
|
+
// side for and from local Expo modules (a directory with expo-module.config.json).
|
|
175
|
+
// each name is the key the matching peach.config slot takes.
|
|
176
|
+
export interface LocalNative {
|
|
177
|
+
kind: 'module' | 'view'
|
|
178
|
+
name: string
|
|
179
|
+
// first place the name appears, relative to the app, as file:line
|
|
180
|
+
path: string
|
|
181
|
+
// the local Expo module directory the name belongs to, relative to the app
|
|
182
|
+
expoModule: string | null
|
|
183
|
+
swift: boolean
|
|
184
|
+
// the app's peach.config.ts already names it
|
|
185
|
+
answered: boolean
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const SOURCE_FILE = /\.[cm]?[jt]sx?$/
|
|
189
|
+
const NATIVE_SOURCE_FILE = /\.(?:swift|kt)$/
|
|
190
|
+
// a requireNativeView(module, view) name is `module_view`, the key the engine
|
|
191
|
+
// registers expo views under.
|
|
192
|
+
const LOOKUPS: { kind: LocalNative['kind']; pattern: RegExp }[] = [
|
|
193
|
+
{
|
|
194
|
+
kind: 'view',
|
|
195
|
+
pattern: /\brequireNativeComponent\s*(?:<[^;]{0,200}?>)?\(\s*['"]([\w$.-]+)['"]/g,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
kind: 'view',
|
|
199
|
+
pattern:
|
|
200
|
+
/\brequireNativeView(?:Manager)?\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]\s*(?:,\s*['"](\w+)['"])?/g,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
kind: 'module',
|
|
204
|
+
pattern:
|
|
205
|
+
/\brequire(?:Optional)?NativeModule\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]/g,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
kind: 'module',
|
|
209
|
+
pattern:
|
|
210
|
+
/\bTurboModuleRegistry\.get(?:Enforcing)?\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]/g,
|
|
211
|
+
},
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
async function scanLocalNative(
|
|
215
|
+
appDir: string,
|
|
216
|
+
pkg: ProjectManifest,
|
|
217
|
+
): Promise<LocalNative[]> {
|
|
218
|
+
const { config } = await resolveRNXAppConfig(appDir)
|
|
219
|
+
const answered = {
|
|
220
|
+
module: new Set(Object.keys({ ...config?.turboModules, ...config?.nativeModules })),
|
|
221
|
+
view: new Set(Object.keys(config?.nativeViews ?? {})),
|
|
222
|
+
}
|
|
223
|
+
const expoModuleDirs: string[] = []
|
|
224
|
+
const sourceFiles: string[] = []
|
|
225
|
+
for (const root of [appDir, ...workspacePackageDirs(appDir, pkg)]) {
|
|
226
|
+
for (const dir of scanRepository(root).directories) {
|
|
227
|
+
if (existsSync(join(dir, 'expo-module.config.json'))) expoModuleDirs.push(dir)
|
|
228
|
+
for (const name of listDir(dir)) {
|
|
229
|
+
if (SOURCE_FILE.test(name) && !name.endsWith('.d.ts'))
|
|
230
|
+
sourceFiles.push(join(dir, name))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const nativeFilesByModule = new Map(
|
|
235
|
+
expoModuleDirs.map((dir) => [
|
|
236
|
+
dir,
|
|
237
|
+
scanRepository(dir).directories.flatMap((sub) =>
|
|
238
|
+
listDir(sub)
|
|
239
|
+
.filter((name) => NATIVE_SOURCE_FILE.test(name))
|
|
240
|
+
.map((name) => join(sub, name)),
|
|
241
|
+
),
|
|
242
|
+
]),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const found = new Map<string, LocalNative>()
|
|
246
|
+
const add = (
|
|
247
|
+
kind: LocalNative['kind'],
|
|
248
|
+
name: string,
|
|
249
|
+
file: string,
|
|
250
|
+
text: string,
|
|
251
|
+
at = 0,
|
|
252
|
+
) => {
|
|
253
|
+
const key = `${kind}:${name}`
|
|
254
|
+
if (found.has(key)) return
|
|
255
|
+
const moduleDir = expoModuleDirs.find((dir) => file.startsWith(`${dir}/`)) ?? null
|
|
256
|
+
found.set(key, {
|
|
257
|
+
kind,
|
|
258
|
+
name,
|
|
259
|
+
path: `${relative(appDir, file)}:${text.slice(0, at).split('\n').length}`,
|
|
260
|
+
expoModule: moduleDir ? relative(appDir, moduleDir) || '.' : null,
|
|
261
|
+
swift: moduleDir
|
|
262
|
+
? (nativeFilesByModule.get(moduleDir) ?? []).some((path) =>
|
|
263
|
+
path.endsWith('.swift'),
|
|
264
|
+
)
|
|
265
|
+
: false,
|
|
266
|
+
answered: answered[kind].has(name),
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
for (const file of sourceFiles) {
|
|
270
|
+
const text = readSmallFile(file)
|
|
271
|
+
if (!text || !/requireNative|TurboModuleRegistry/.test(text)) continue
|
|
272
|
+
for (const { kind, pattern } of LOOKUPS) {
|
|
273
|
+
for (const match of text.matchAll(pattern)) {
|
|
274
|
+
const name = match[2] ? `${match[1]}_${match[2]}` : match[1]
|
|
275
|
+
if (name) add(kind, name, file, text, match.index)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// a local expo module declares its name in its native definition, Name("X"),
|
|
280
|
+
// whether or not the app's javascript spells it out.
|
|
281
|
+
for (const [, files] of nativeFilesByModule) {
|
|
282
|
+
for (const file of files) {
|
|
283
|
+
const text = readSmallFile(file)
|
|
284
|
+
if (!text) continue
|
|
285
|
+
for (const match of text.matchAll(/\bName\(\s*"(\w+)"\s*\)/g)) {
|
|
286
|
+
if (match[1]) add('module', match[1], file, text, match.index)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return [...found.values()].sort(
|
|
291
|
+
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function listDir(dir: string): string[] {
|
|
296
|
+
try {
|
|
297
|
+
return readdirSync(dir)
|
|
298
|
+
} catch {
|
|
299
|
+
return []
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function readSmallFile(file: string): string | null {
|
|
304
|
+
try {
|
|
305
|
+
return statSync(file).size <= 1024 * 1024 ? readFileSync(file, 'utf8') : null
|
|
306
|
+
} catch {
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// dependencies installed as links to source outside any node_modules are the
|
|
312
|
+
// app's own workspace packages, which hold its native code as often as the app
|
|
313
|
+
// directory does.
|
|
314
|
+
function workspacePackageDirs(appDir: string, pkg: ProjectManifest): string[] {
|
|
315
|
+
const dirs = new Set<string>()
|
|
316
|
+
for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
317
|
+
let current = appDir
|
|
318
|
+
while (true) {
|
|
319
|
+
const link = join(current, 'node_modules', name)
|
|
320
|
+
if (existsSync(link)) {
|
|
321
|
+
if (lstatSync(link).isSymbolicLink()) {
|
|
322
|
+
const target = realpathSync(link)
|
|
323
|
+
if (
|
|
324
|
+
!target.split('/').includes('node_modules') &&
|
|
325
|
+
!target.startsWith(`${appDir}/`)
|
|
326
|
+
) {
|
|
327
|
+
dirs.add(target)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
const parent = dirname(current)
|
|
333
|
+
if (parent === current) break
|
|
334
|
+
current = parent
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return [...dirs]
|
|
338
|
+
}
|
|
339
|
+
|
|
116
340
|
function scanOnePackage(
|
|
117
341
|
appDir: string,
|
|
118
342
|
pkg: ProjectManifest,
|
package/cli/commands/control.ts
CHANGED
|
@@ -15,10 +15,10 @@ import { isAppDeepLinkTarget, openAppUrl } from '../../src/app-url'
|
|
|
15
15
|
import { isLoopbackHost } from '../../src/backend-origin'
|
|
16
16
|
import { resolveSootsimShellBaseUrlForBridgePort } from '../../src/bridge-constants'
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type
|
|
18
|
+
applyPeachConfigToUrl,
|
|
19
|
+
mergePeachConfig,
|
|
20
|
+
PEACH_CONFIG_QUERY_PARAM,
|
|
21
|
+
type PeachConfig,
|
|
22
22
|
} from '../../src/config'
|
|
23
23
|
import {
|
|
24
24
|
resolveConnectionInput,
|
|
@@ -97,7 +97,7 @@ import type { DriverId, DriverLaunchResult } from '../drivers/types'
|
|
|
97
97
|
interface ControlCommandOptions {
|
|
98
98
|
port?: number
|
|
99
99
|
timeoutMs?: number
|
|
100
|
-
runtimeConfig?:
|
|
100
|
+
runtimeConfig?: PeachConfig
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export interface OpenCommandResources {
|
|
@@ -555,8 +555,8 @@ function normalizeReplacementPath(filePath: string): string {
|
|
|
555
555
|
return path.resolve(process.cwd(), filePath)
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
function parseOpenRuntimeConfig(args: string[]):
|
|
559
|
-
const modules: NonNullable<
|
|
558
|
+
function parseOpenRuntimeConfig(args: string[]): PeachConfig | undefined {
|
|
559
|
+
const modules: NonNullable<PeachConfig['modules']> = {}
|
|
560
560
|
const remap: Record<string, string> = {}
|
|
561
561
|
|
|
562
562
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -597,7 +597,7 @@ function parseOpenRuntimeConfig(args: string[]): RNXConfig | undefined {
|
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
const config:
|
|
600
|
+
const config: PeachConfig = {}
|
|
601
601
|
if (Object.keys(modules).length > 0) config.modules = modules
|
|
602
602
|
if (Object.keys(remap).length > 0) config.network = { remap }
|
|
603
603
|
return config.modules || config.network ? config : undefined
|
|
@@ -737,9 +737,9 @@ async function resolveShellUrl(
|
|
|
737
737
|
if (looksLikeSootsimUrl(target)) {
|
|
738
738
|
const url = new URL(target)
|
|
739
739
|
const base = new URL(baseUrl)
|
|
740
|
-
const baseConfig = base.searchParams.get(
|
|
741
|
-
if (baseConfig && !url.searchParams.has(
|
|
742
|
-
url.searchParams.set(
|
|
740
|
+
const baseConfig = base.searchParams.get(PEACH_CONFIG_QUERY_PARAM)
|
|
741
|
+
if (baseConfig && !url.searchParams.has(PEACH_CONFIG_QUERY_PARAM)) {
|
|
742
|
+
url.searchParams.set(PEACH_CONFIG_QUERY_PARAM, baseConfig)
|
|
743
743
|
}
|
|
744
744
|
return url.toString()
|
|
745
745
|
}
|
|
@@ -1281,16 +1281,16 @@ export function printOpenedSim(
|
|
|
1281
1281
|
try {
|
|
1282
1282
|
const parsed = new URL(value)
|
|
1283
1283
|
let changed = false
|
|
1284
|
-
if (parsed.searchParams.has(
|
|
1285
|
-
parsed.searchParams.set(
|
|
1284
|
+
if (parsed.searchParams.has(PEACH_CONFIG_QUERY_PARAM)) {
|
|
1285
|
+
parsed.searchParams.set(PEACH_CONFIG_QUERY_PARAM, '[redacted]')
|
|
1286
1286
|
changed = true
|
|
1287
1287
|
}
|
|
1288
1288
|
const bundle = parsed.searchParams.get('bundle')
|
|
1289
1289
|
if (bundle) {
|
|
1290
1290
|
try {
|
|
1291
1291
|
const parsedBundle = new URL(bundle)
|
|
1292
|
-
if (parsedBundle.searchParams.has(
|
|
1293
|
-
parsedBundle.searchParams.set(
|
|
1292
|
+
if (parsedBundle.searchParams.has(PEACH_CONFIG_QUERY_PARAM)) {
|
|
1293
|
+
parsedBundle.searchParams.set(PEACH_CONFIG_QUERY_PARAM, '[redacted]')
|
|
1294
1294
|
parsed.searchParams.set('bundle', parsedBundle.toString())
|
|
1295
1295
|
changed = true
|
|
1296
1296
|
}
|
|
@@ -1585,8 +1585,8 @@ export async function runOpenCommand(
|
|
|
1585
1585
|
const project = await resolveAppProjectForBundleUrl(resolvedBundle?.bundleUrl)
|
|
1586
1586
|
const appConfig = await loadRNXAppConfig(project)
|
|
1587
1587
|
const configuredRuntimeVersion = appConfig?.runtimeVersion
|
|
1588
|
-
const runtimeConfig =
|
|
1589
|
-
|
|
1588
|
+
const runtimeConfig = mergePeachConfig(
|
|
1589
|
+
mergePeachConfig(
|
|
1590
1590
|
appConfig ? { ...appConfig, runtimeVersion: undefined } : undefined,
|
|
1591
1591
|
opts.runtimeConfig,
|
|
1592
1592
|
),
|
|
@@ -1842,7 +1842,7 @@ export async function runOpenCommand(
|
|
|
1842
1842
|
if (!targetSim && parsed.simIdSource !== 'flag') {
|
|
1843
1843
|
targetShellUrl = await buildShellUrl(
|
|
1844
1844
|
target,
|
|
1845
|
-
|
|
1845
|
+
applyPeachConfigToUrl(baseUrl, runtimeConfig),
|
|
1846
1846
|
buildShellUrlOpts,
|
|
1847
1847
|
)
|
|
1848
1848
|
const normalizedTarget = new URL(targetShellUrl)
|
|
@@ -1881,7 +1881,7 @@ export async function runOpenCommand(
|
|
|
1881
1881
|
targetShellUrl ??
|
|
1882
1882
|
(await buildShellUrl(
|
|
1883
1883
|
target,
|
|
1884
|
-
|
|
1884
|
+
applyPeachConfigToUrl(resolvedBaseUrl, runtimeConfig),
|
|
1885
1885
|
buildShellUrlOpts,
|
|
1886
1886
|
))
|
|
1887
1887
|
const navigationUrl = new URL(navigatedShellUrl)
|
|
@@ -1968,7 +1968,7 @@ export async function runOpenCommand(
|
|
|
1968
1968
|
}
|
|
1969
1969
|
}
|
|
1970
1970
|
|
|
1971
|
-
const configuredBaseUrl =
|
|
1971
|
+
const configuredBaseUrl = applyPeachConfigToUrl(baseUrl, runtimeConfig)
|
|
1972
1972
|
const openUrl = await buildOpenUrl(target, configuredBaseUrl, token, buildShellUrlOpts)
|
|
1973
1973
|
const ownerPid = opts.ownerPid ?? getStableOwnerPid()
|
|
1974
1974
|
if (!ownerPid) {
|