peachsim 0.1.1481 → 0.1.1594
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 +26 -6
- package/cli/app-config.ts +43 -8
- package/cli/app-fonts.ts +13 -17
- package/cli/app-stubs.ts +302 -0
- package/cli/bin.ts +6 -2
- package/cli/bridge-flow-runner.ts +195 -40
- package/cli/cloud-test-tunnel.ts +2 -0
- package/cli/command-registry.ts +2 -1
- package/cli/commands/box/bundle-plane.ts +4 -0
- package/cli/commands/box.ts +3 -20
- package/cli/commands/control.ts +89 -18
- package/cli/commands/debug.ts +2 -2
- package/cli/commands/do-chain.ts +2 -2
- package/cli/commands/doctor.ts +967 -0
- package/cli/commands/flow.ts +434 -403
- package/cli/commands/inspect/core.ts +45 -19
- package/cli/commands/inspect/env.ts +10 -0
- package/cli/commands/inspect/list.ts +16 -5
- package/cli/commands/inspect/settling.ts +64 -29
- package/cli/commands/inspect.ts +32 -12
- package/cli/commands/install-desktop.ts +3 -2
- package/cli/commands/kotlin.ts +77 -16
- package/cli/commands/login.ts +1 -1
- package/cli/commands/maestro-compare.ts +16 -3
- package/cli/commands/maestro-generate.ts +11 -7
- package/cli/commands/maestro.ts +43 -15
- package/cli/commands/no-bridge-hint.ts +26 -3
- package/cli/commands/platform.ts +16 -6
- package/cli/commands/record.ts +9 -1
- package/cli/commands/report-issue.ts +2 -1
- package/cli/commands/serve.ts +30 -2
- package/cli/commands/setup.ts +15 -2
- package/cli/commands/swift.ts +4 -0
- package/cli/commands/upload.ts +91 -18
- package/cli/current-sim.ts +1 -2
- package/cli/device-option.ts +8 -0
- package/cli/drivers/desktop.ts +1 -1
- package/cli/drivers/playwright-sim-host.ts +30 -11
- package/cli/drivers/playwright.ts +1 -4
- package/cli/flow-export.ts +2 -2
- package/cli/flow-file.ts +18 -18
- package/cli/internal-child.ts +7 -6
- package/cli/maestro-compare.ts +9 -3
- package/cli/maestro-js.ts +2 -1
- package/cli/main.ts +3 -3
- package/cli/outbound-endpoints.ts +105 -41
- package/cli/parse-args.ts +7 -0
- package/cli/preview-capture.ts +12 -0
- package/cli/privacy.ts +1 -1
- package/cli/runtime-compat.ts +16 -0
- package/cli/ui.ts +5 -0
- package/cli/ws-bridge.ts +107 -44
- package/detox/attempt-events.cjs +706 -0
- package/detox/attempt-events.d.cts +99 -0
- package/detox/index.ts +492 -58
- package/detox/motion-change.cjs +251 -0
- package/detox/motion-change.d.cts +73 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.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 +3 -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/bundle-channel.cjs +2018 -481
- 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 +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +14 -2
- package/dist-lib/config.mjs +13 -2
- package/dist-lib/detox/index.cjs +1623 -113
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +1937 -461
- package/dist-lib/expo-swift-metadata.cjs +21 -13
- package/dist-lib/expo-swift-metadata.mjs +21 -13
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +17679 -15689
- package/dist-lib/host/fetch-proxy-handler.cjs +122 -97
- package/dist-lib/host/fetch-proxy-overrides.cjs +23 -3
- package/dist-lib/host/fetch-proxy-overrides.mjs +19 -2
- package/dist-lib/host/replacement-module-handler.cjs +19 -18
- package/dist-lib/host/websocket-proxy.cjs +228 -7
- package/dist-lib/index.cjs +5540 -4009
- package/dist-lib/jump-to-source-babel.cjs +73 -35
- package/dist-lib/jump-to-source-native.cjs +86 -7
- package/dist-lib/kotlin-host.cjs +3 -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 +51 -97
- package/dist-lib/metro-production-bundle.mjs +41 -97
- package/dist-lib/metro.cjs +4032 -364
- package/dist-lib/native-app-identity-project.cjs +232 -118
- package/dist-lib/native-app-identity-project.mjs +231 -118
- package/dist-lib/native-app-identity.cjs +304 -33
- package/dist-lib/native-app-identity.mjs +301 -32
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- 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 +11 -1
- package/dist-lib/react-native-host-modules.mjs +11 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +98 -34
- package/dist-lib/sdk.cjs +98 -48
- package/dist-lib/sdk.mjs +98 -48
- package/dist-lib/skills.cjs +13104 -9997
- package/dist-lib/swift-compile.cjs +1851 -437
- package/dist-lib/swift-compile.mjs +1860 -445
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +2 -2
- package/dist-lib/swift-submit.mjs +2 -2
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1938 -462
- package/dist-lib/vite.cjs +121 -52
- package/dist-lib/worklet-executable.cjs +18 -4
- package/dist-lib/worklet-executable.mjs +18 -4
- package/install.sh +7 -2
- package/kotlin/AndroidViewSources/android/graphics/Canvas.kt +2 -0
- package/kotlin/AndroidViewSources/android/graphics/Paint.kt +18 -0
- package/kotlin/AndroidViewSources/dev/peach/app/JavaFormat.kt +16 -8
- package/kotlin/ComposeSources/PeachComposeHost.kt +31 -12
- package/kotlin/ComposeSources/PeachMagnifier.kt +185 -0
- package/kotlin/ExpoComposeSources/PeachAndroidView.kt +16 -0
- package/kotlin/Sources/java/util/IllegalFormatCodePointException.kt +10 -0
- package/kotlin/Sources/java/util/IllegalFormatException.kt +5 -0
- package/kotlin/fixtures/counter/CounterApp.native.tsx +30 -3
- package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +1 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +2 -1
- package/kotlin/fixtures/counter/package.json +15 -2
- package/kotlin/fixtures/counter/vite.config.ts +11 -1
- package/kotlin/fork/NOTICE +1 -0
- package/kotlin/fork/README.md +5 -4
- package/kotlin/fork/compose-worker-globals.patch +629 -0
- package/kotlin/fork/fork.json +1 -1
- package/kotlin/toolchain.json +3 -3
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +137 -42
- package/skills/contrast/SKILL.md +3 -3
- package/skills/peach-debug/SKILL.md +1 -1
- package/skills/peach-setup/SKILL.md +22 -12
- package/skills/peach-test/SKILL.md +2 -3
- package/src/agent-identity.ts +18 -0
- package/src/bridge-constants.ts +1 -1
- package/src/bundle-channel.mjs +37 -6
- package/src/bundleGlobalFooter.ts +70 -20
- package/src/config.ts +38 -0
- package/src/configured-bundle-producer.ts +4 -1
- package/src/desktop-host-delivery.ts +4 -3
- package/src/host/bridge-host.ts +218 -75
- package/src/host/fetch-proxy-handler.ts +145 -117
- package/src/host/fetch-proxy-overrides.ts +27 -0
- package/src/host/fetch-proxy-transport.ts +77 -63
- package/src/host/lease-owner.test.ts +18 -1
- package/src/host/lease-owner.ts +4 -5
- package/src/host/loopback-ports.ts +34 -0
- package/src/host/replacement-module-handler.ts +26 -18
- package/src/host/server-scan-handler.ts +87 -0
- package/src/host/websocket-proxy.ts +251 -6
- package/src/installed-package-versions.ts +65 -0
- package/src/jump-to-source-babel.ts +97 -89
- package/src/jump-to-source-native.ts +107 -9
- package/src/kotlin-host.ts +1 -1
- package/src/kotlinDiscoveryContract.ts +27 -0
- package/src/metro-plugin.ts +129 -39
- package/src/metro-production-bundle.ts +89 -137
- package/src/native-app-identity-project.ts +38 -2
- package/src/native-app-identity.ts +142 -11
- package/src/native-firebase-config.ts +82 -80
- package/src/nativeAppIconContract.ts +132 -0
- package/src/nitro-bundle.ts +43 -0
- package/src/nitro-compile.ts +107 -0
- package/src/nitro-source.ts +314 -0
- package/src/nitroContract.ts +51 -0
- package/src/oneWidgetContract.ts +14 -3
- package/src/packageVersionsContract.ts +52 -0
- package/src/react-devtools-frontend-react.ts +45 -0
- package/src/react-native-host-modules.ts +10 -0
- package/src/skills/builtin/maestro-test.ts +2 -4
- package/src/swift-compile.ts +2084 -331
- package/src/swift-distribution-contract.ts +11 -0
- package/src/swift-distribution.lock.json +45 -11
- package/src/swift-host.ts +13 -7
- package/src/swift-icu.ts +10 -2
- package/src/swift-submit.ts +2 -1
- package/src/vite-plugin-one.ts +9 -3
- package/src/vite-plugin.ts +63 -29
- package/src/worker-system-chunks.ts +3 -1
- package/src/worklet-executable.ts +21 -3
- package/src/xcode-project.ts +47 -125
- package/src/xcode-target-members.ts +107 -0
- package/cli/commands/compat.ts +0 -395
package/README.md
CHANGED
|
@@ -185,6 +185,12 @@ config dictionary is empty and the app can select its own public defaults.
|
|
|
185
185
|
Preserve failures from those endpoints rather than remapping them to a successful
|
|
186
186
|
local response. The same flag is available on `peach flow`.
|
|
187
187
|
|
|
188
|
+
A project with several Xcode application targets names the one to run, as an
|
|
189
|
+
Xcode scheme does: `peach open <app-port> --ios-target <name>` (also on
|
|
190
|
+
`peach flow` and `peach maestro test`) reads that target's version, build,
|
|
191
|
+
bundle id and display name. Without it, such an app served by a plain Metro
|
|
192
|
+
reads the runtime defaults.
|
|
193
|
+
|
|
188
194
|
**The daemon is one process** — `SootSimBridgeHost` (`src/host/bridge-host.ts`).
|
|
189
195
|
`peach serve` runs it in the foreground; the background daemon is the *same
|
|
190
196
|
process* wrapped with autostart and the `~/.peach/daemon.json` lockfile. It is
|
|
@@ -196,6 +202,12 @@ guest-app fetch/WebSocket traffic so the cross-origin tenant worker can reach
|
|
|
196
202
|
localhost. The `AgentHost` extension owns one FIFO reader per agent session and
|
|
197
203
|
fans agent events out to every subscriber.
|
|
198
204
|
|
|
205
|
+
`peach open` auto-installs a published runtime only when the runtime directory
|
|
206
|
+
is empty. If runtime files exist without a selected runtime containing
|
|
207
|
+
`index.html`, it refuses to open. Finish copying the runtime, then select it
|
|
208
|
+
with `peach runtime use <version>`. Write the active marker after staging is
|
|
209
|
+
complete when preparing an isolated home.
|
|
210
|
+
|
|
199
211
|
During source development, each Vite bridge owns one
|
|
200
212
|
`~/.peach/dev-bridges/<port>.json` heartbeat record. CLI defaults select the
|
|
201
213
|
current `PORT_OFFSET`, then the current checkout, then the canonical `:7668`
|
|
@@ -267,6 +279,7 @@ therefore runs before assertions in each flow.
|
|
|
267
279
|
curl -fsSL https://peachsim.com/install.sh | sh # or: npx peachsim
|
|
268
280
|
peach # open ConnectRN and choose a running app in the simulator
|
|
269
281
|
peach open /settings # dispatch a React Native deep link into that app
|
|
282
|
+
peach open /settings --no-describe --quiet # keep open options with deep links
|
|
270
283
|
```
|
|
271
284
|
|
|
272
285
|
Start Metro, Expo, One, or another supported React Native development server
|
|
@@ -389,6 +402,8 @@ outside `node_modules`, `dist`, and `build`. Keep it before
|
|
|
389
402
|
`react-native-worklets/plugin` in a custom Babel stack that lists both plugins.
|
|
390
403
|
Apps using `babel-preset-expo` omit the explicit Worklets plugin because the
|
|
391
404
|
preset configures it automatically.
|
|
405
|
+
Runtime Fragment aliases are excluded before element creation. Component tag
|
|
406
|
+
expressions are evaluated once, and ordinary components retain their locations.
|
|
392
407
|
|
|
393
408
|
For One's Rolldown native bundler, use the native transform in `vite.config.ts`:
|
|
394
409
|
|
|
@@ -421,7 +436,7 @@ peach open 8081 # load a running metro/expo dev server
|
|
|
421
436
|
peach open /settings # route via React Native Linking
|
|
422
437
|
peach describe # dump UI tree
|
|
423
438
|
peach get errors 5 # recent runtime failures
|
|
424
|
-
peach
|
|
439
|
+
peach doctor # check what of this app Peach runs
|
|
425
440
|
peach support report "…" # preview an opt-in compatibility report
|
|
426
441
|
peach find --testid cta # inspect one node deeply
|
|
427
442
|
peach debug enable animated,layout
|
|
@@ -499,6 +514,11 @@ options and enables two integrations:
|
|
|
499
514
|
The URL helpers request ordinary Metro graphs: `dev=false&minify=true` for
|
|
500
515
|
production and `dev=true&minify=false` for development.
|
|
501
516
|
|
|
517
|
+
Metro registration readers retain the original factory bytes and module IDs.
|
|
518
|
+
Empty dependencies may be an empty array, an omitted production argument, or a
|
|
519
|
+
null development argument before the module name. Identity validation and
|
|
520
|
+
fingerprinting treat each as zero dependencies and reject malformed arguments.
|
|
521
|
+
|
|
502
522
|
```js
|
|
503
523
|
const {
|
|
504
524
|
toPeachProductionBundleUrl,
|
|
@@ -695,11 +715,11 @@ serves its selected version from a version-specific localhost origin, so a
|
|
|
695
715
|
background activation cannot mix an already loaded shell with chunks from the
|
|
696
716
|
next runtime. Without config, each new open selects the active stable version.
|
|
697
717
|
When `peach.config.ts` sets `runtimeVersion`, `peach open` installs that
|
|
698
|
-
version without changing the machine default. The same file
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
718
|
+
version without changing the machine default. The same file answers native code Peach does
|
|
719
|
+
not know under `packages`, keyed by the package and then its native module
|
|
720
|
+
and view names, and an installed `peach-stub-<package>` package answers in the
|
|
721
|
+
same shape with no config line; the configuration guide in the docs lists
|
|
722
|
+
every slot. App config, native-linked fonts,
|
|
703
723
|
and splash assets resolve from the local directory named by `--project-root`
|
|
704
724
|
when supplied, otherwise the project root advertised by the selected development
|
|
705
725
|
server. Hosted recorders pass their checked-out repository explicitly because
|
package/cli/app-config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as peachConfigModule from '../src/config'
|
|
|
4
4
|
import { isPeachConfig, type PeachConfig } from '../src/config'
|
|
5
5
|
import { rnxPublicBrand } from '../src/public-brand'
|
|
6
6
|
import { resolveAppProject, type ResolvedAppProject } from './app-project'
|
|
7
|
+
import { resolveAppStubs, type AppStubs } from './app-stubs'
|
|
7
8
|
|
|
8
9
|
// `{ file }` entries are relative to the config file; the engine loads them by
|
|
9
10
|
// absolute path from the runtime's replacement-module endpoint.
|
|
@@ -33,16 +34,42 @@ function resolveConfigReplacementFiles(
|
|
|
33
34
|
return {
|
|
34
35
|
...config,
|
|
35
36
|
modules: resolveFileEntries(config.modules, configDir),
|
|
36
|
-
turboModules: resolveFileEntries(config.turboModules, configDir),
|
|
37
|
-
nativeModules: resolveFileEntries(config.nativeModules, configDir),
|
|
38
|
-
nativeViews: resolveFileEntries(config.nativeViews, configDir),
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
|
|
40
|
+
// the engine's wire slots; an app names native stubs by package instead
|
|
41
|
+
const WIRE_SLOTS = ['nativeModules', 'turboModules', 'nativeViews'] as const
|
|
42
|
+
|
|
43
|
+
// the app's config with every native stub lowered into the engine's wire slots:
|
|
44
|
+
// its own `packages` and the installed stub packages, applied with or without
|
|
45
|
+
// a peach.config.ts.
|
|
42
46
|
export async function loadRNXAppConfig(
|
|
43
47
|
project: ResolvedAppProject | null,
|
|
44
48
|
): Promise<PeachConfig | undefined> {
|
|
45
|
-
|
|
49
|
+
return (await loadRNXAppStubs(project)).config
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function loadRNXAppStubs(
|
|
53
|
+
project: ResolvedAppProject | null,
|
|
54
|
+
): Promise<{ config: PeachConfig | undefined; stubs: AppStubs }> {
|
|
55
|
+
if (!project) return { config: undefined, stubs: { applied: [], skipped: [] } }
|
|
56
|
+
const config = await loadConfigFile(project)
|
|
57
|
+
try {
|
|
58
|
+
return resolveAppStubs(
|
|
59
|
+
project.appDir,
|
|
60
|
+
config,
|
|
61
|
+
project.configPath ? path.dirname(project.configPath) : project.appDir,
|
|
62
|
+
)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
65
|
+
throw new Error(`could not load the native stubs of ${project.appDir}: ${message}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function loadConfigFile(
|
|
70
|
+
project: ResolvedAppProject,
|
|
71
|
+
): Promise<PeachConfig | undefined> {
|
|
72
|
+
if (!project.configPath) return undefined
|
|
46
73
|
try {
|
|
47
74
|
const jiti = createJiti(import.meta.url, {
|
|
48
75
|
interopDefault: true,
|
|
@@ -57,6 +84,12 @@ export async function loadRNXAppConfig(
|
|
|
57
84
|
if (!isPeachConfig(loaded)) {
|
|
58
85
|
throw new Error('the default export must be a peach config object')
|
|
59
86
|
}
|
|
87
|
+
const wire = WIRE_SLOTS.filter((slot) => loaded[slot] !== undefined)
|
|
88
|
+
if (wire.length > 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${wire.join(', ')} moved under packages, keyed by the package whose native side they answer: packages: { 'package-name': { modules: { Name: './stub.ts' }, views: { Name: './View.tsx' } } }`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
60
93
|
return resolveConfigReplacementFiles(loaded, project.configPath)
|
|
61
94
|
} catch (error) {
|
|
62
95
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -64,9 +97,11 @@ export async function loadRNXAppConfig(
|
|
|
64
97
|
}
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
export async function resolveRNXAppConfig(
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
export async function resolveRNXAppConfig(startDir: string = process.cwd()): Promise<{
|
|
101
|
+
project: ResolvedAppProject | null
|
|
102
|
+
config: PeachConfig | undefined
|
|
103
|
+
stubs: AppStubs
|
|
104
|
+
}> {
|
|
70
105
|
const project = resolveAppProject(startDir)
|
|
71
|
-
return { project,
|
|
106
|
+
return { project, ...(await loadRNXAppStubs(project)) }
|
|
72
107
|
}
|
package/cli/app-fonts.ts
CHANGED
|
@@ -3,10 +3,12 @@ import fs from 'node:fs'
|
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import {
|
|
6
|
+
encodeAppFontsWire,
|
|
6
7
|
expoFontPathsFromPlugins,
|
|
7
8
|
nativeLinkedFontSpecsFromPaths,
|
|
8
9
|
metroFontSpecsFromPaths,
|
|
9
10
|
type ConfigPluginFontSpec,
|
|
11
|
+
type FontPlatform,
|
|
10
12
|
} from 'peach-engine/engine/config-plugin-fonts'
|
|
11
13
|
import { resolveListeningPid, resolveProcessCwd } from '../scripts/dev-server-scanner'
|
|
12
14
|
import { loopbackOriginOf } from '../src/backend-origin'
|
|
@@ -34,15 +36,17 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
34
36
|
}): DiscoveredAppFontSpec[] {
|
|
35
37
|
const metroOrigin = loopbackOriginOf(opts.bundleUrl)
|
|
36
38
|
if (!metroOrigin) return []
|
|
37
|
-
|
|
39
|
+
return metroFontSpecsFromPaths(
|
|
38
40
|
projectRelativeFontPaths(opts.projectDir, discoverNativeLinkedAppFontPaths(opts)),
|
|
39
41
|
metroOrigin,
|
|
42
|
+
nativeFontPlatform(opts.platform),
|
|
40
43
|
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function nativeFontPlatform(platform: string | undefined): FontPlatform {
|
|
47
|
+
return (platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase() === 'android'
|
|
48
|
+
? 'android'
|
|
49
|
+
: 'ios'
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function projectRelativeFontPaths(projectDir: string, fontPaths: string[]): string[] {
|
|
@@ -68,7 +72,7 @@ function discoverNativeLinkedAppFontPaths(opts: {
|
|
|
68
72
|
const selectedAppDir = project?.appDir ?? null
|
|
69
73
|
if (!selectedAppDir) return []
|
|
70
74
|
|
|
71
|
-
const platform = (opts.platform
|
|
75
|
+
const platform = nativeFontPlatform(opts.platform)
|
|
72
76
|
|
|
73
77
|
let getConfig: ExpoConfigGetter | null = null
|
|
74
78
|
// resolve from the exact app root advertised by the development server.
|
|
@@ -97,10 +101,7 @@ function discoverNativeLinkedAppFontPaths(opts: {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
// a declared path with no file behind it links nothing natively.
|
|
100
|
-
const fontPaths = expoFontPathsFromPlugins(
|
|
101
|
-
plugins,
|
|
102
|
-
platform === 'android' ? 'android' : 'ios',
|
|
103
|
-
)
|
|
104
|
+
const fontPaths = expoFontPathsFromPlugins(plugins, platform)
|
|
104
105
|
.slice(0, 256)
|
|
105
106
|
.map((fontPath) => path.resolve(selectedAppDir, fontPath))
|
|
106
107
|
.filter(isFile)
|
|
@@ -285,6 +286,7 @@ export function readNativeLinkedAppFontsWire(opts: {
|
|
|
285
286
|
return encodeAppFontsWire(
|
|
286
287
|
nativeLinkedFontSpecsFromPaths(
|
|
287
288
|
projectRelativeFontPaths(opts.projectDir, discoverNativeLinkedAppFontPaths(opts)),
|
|
289
|
+
nativeFontPlatform(opts.platform),
|
|
288
290
|
(fontPath) => {
|
|
289
291
|
const bytes = fs.readFileSync(path.resolve(opts.projectDir, fontPath))
|
|
290
292
|
return `data:${fontContentType(fontPath)};base64,${bytes.toString('base64')}`
|
|
@@ -355,12 +357,6 @@ export async function resolveMetroProjectRoot(bundleUrl: string): Promise<string
|
|
|
355
357
|
return pid === null ? null : resolveProcessCwd(pid)
|
|
356
358
|
}
|
|
357
359
|
|
|
358
|
-
export function encodeAppFontsWire(specs: DiscoveredAppFontSpec[]): string {
|
|
359
|
-
return specs
|
|
360
|
-
.map((spec) => `${encodeURIComponent(spec.url)}::${encodeURIComponent(spec.family)}`)
|
|
361
|
-
.join(',')
|
|
362
|
-
}
|
|
363
|
-
|
|
364
360
|
export function parseAppFontUrls(wire: string): string[] {
|
|
365
361
|
const urls: string[] = []
|
|
366
362
|
for (const entry of wire.split(',')) {
|
package/cli/app-stubs.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// native stubs keyed by the package they answer (specs/peach-app-stubs.md):
|
|
2
|
+
// the app's peach.config.ts `packages`, installed `peach-stub-<package>`
|
|
3
|
+
// packages, and a library's own `peach` field, lowered into the engine's
|
|
4
|
+
// nativeModules / nativeViews wire slots.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
7
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
8
|
+
import { resolveCompatVersionRange } from '@peach/compat/version-range'
|
|
9
|
+
import {
|
|
10
|
+
isPackageStubs,
|
|
11
|
+
type NativeModuleResolution,
|
|
12
|
+
type NativeStub,
|
|
13
|
+
type NativeViewResolution,
|
|
14
|
+
type PackageStubs,
|
|
15
|
+
type PeachConfig,
|
|
16
|
+
} from '../src/config'
|
|
17
|
+
import { isRecord } from '../src/is-record'
|
|
18
|
+
import { readProjectManifest, scanRepository } from './commands/detect'
|
|
19
|
+
|
|
20
|
+
// who answers a package, in rising precedence
|
|
21
|
+
export type StubOrigin = 'library' | 'stub-package' | 'config'
|
|
22
|
+
|
|
23
|
+
export interface AppliedStubs {
|
|
24
|
+
target: string
|
|
25
|
+
origin: StubOrigin
|
|
26
|
+
// the package declaring the stubs; null for peach.config.ts
|
|
27
|
+
provider: string | null
|
|
28
|
+
providerVersion: string | null
|
|
29
|
+
modules: string[]
|
|
30
|
+
views: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// a published stub whose `versions` range excludes the installed target
|
|
34
|
+
export interface SkippedStubs {
|
|
35
|
+
target: string
|
|
36
|
+
provider: string
|
|
37
|
+
providerVersion: string | null
|
|
38
|
+
versions: string
|
|
39
|
+
installed: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AppStubs {
|
|
43
|
+
applied: AppliedStubs[]
|
|
44
|
+
skipped: SkippedStubs[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface StubSource {
|
|
48
|
+
target: string
|
|
49
|
+
origin: StubOrigin
|
|
50
|
+
provider: string | null
|
|
51
|
+
providerVersion: string | null
|
|
52
|
+
stubs: PackageStubs
|
|
53
|
+
// where relative stub paths resolve from
|
|
54
|
+
baseDir: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `peach-stub-<package>` or `@scope/peach-stub-<package>`; a scoped target uses
|
|
58
|
+
// the DefinitelyTyped encoding, `@acme/scanner` as `peach-stub-acme__scanner`.
|
|
59
|
+
export function stubPackageTarget(name: string): string | null {
|
|
60
|
+
const match = /^(?:@[^/]+\/)?peach-stub-(.+)$/.exec(name)
|
|
61
|
+
if (!match?.[1]) return null
|
|
62
|
+
const split = match[1].indexOf('__')
|
|
63
|
+
return split > 0
|
|
64
|
+
? `@${match[1].slice(0, split)}/${match[1].slice(split + 2)}`
|
|
65
|
+
: match[1]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveAppStubs(
|
|
69
|
+
appDir: string,
|
|
70
|
+
config: PeachConfig | undefined,
|
|
71
|
+
configDir: string,
|
|
72
|
+
): { config: PeachConfig | undefined; stubs: AppStubs } {
|
|
73
|
+
const sources: StubSource[] = []
|
|
74
|
+
const skipped: SkippedStubs[] = []
|
|
75
|
+
const manifest = readProjectManifest(appDir)
|
|
76
|
+
for (const name of Object.keys({
|
|
77
|
+
...manifest?.devDependencies,
|
|
78
|
+
...manifest?.dependencies,
|
|
79
|
+
})) {
|
|
80
|
+
const dir = installedPackageDir(appDir, name)
|
|
81
|
+
const installed = dir ? readPackageJson(dir) : null
|
|
82
|
+
if (!dir || !installed || installed.peach === undefined) continue
|
|
83
|
+
const stubTarget = stubPackageTarget(name)
|
|
84
|
+
const target = stubTarget ?? name
|
|
85
|
+
const field = installed.peach
|
|
86
|
+
const versions = isRecord(field) ? field.versions : undefined
|
|
87
|
+
const stubs = isRecord(field) ? { modules: field.modules, views: field.views } : field
|
|
88
|
+
if (
|
|
89
|
+
!isPackageStubs(stubs) ||
|
|
90
|
+
(versions !== undefined && typeof versions !== 'string')
|
|
91
|
+
)
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${name}'s package.json "peach" field must be "noop" or { versions?, modules?, views? } whose values are "noop" or file paths`,
|
|
94
|
+
)
|
|
95
|
+
const providerVersion =
|
|
96
|
+
typeof installed.version === 'string' ? installed.version : null
|
|
97
|
+
const targetDir = stubTarget ? installedPackageDir(appDir, target) : dir
|
|
98
|
+
const targetVersion = targetDir ? readPackageJson(targetDir)?.version : undefined
|
|
99
|
+
// a stub for a package the app does not install answers nothing
|
|
100
|
+
if (typeof targetVersion !== 'string') continue
|
|
101
|
+
if (
|
|
102
|
+
versions !== undefined &&
|
|
103
|
+
resolveCompatVersionRange([versions], targetVersion).status !== 'matched'
|
|
104
|
+
) {
|
|
105
|
+
skipped.push({
|
|
106
|
+
target,
|
|
107
|
+
provider: name,
|
|
108
|
+
providerVersion,
|
|
109
|
+
versions,
|
|
110
|
+
installed: targetVersion,
|
|
111
|
+
})
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
sources.push({
|
|
115
|
+
target,
|
|
116
|
+
origin: stubTarget ? 'stub-package' : 'library',
|
|
117
|
+
provider: name,
|
|
118
|
+
providerVersion,
|
|
119
|
+
stubs,
|
|
120
|
+
baseDir: dir,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
for (const [target, stubs] of Object.entries(config?.packages ?? {})) {
|
|
124
|
+
sources.push({
|
|
125
|
+
target,
|
|
126
|
+
origin: 'config',
|
|
127
|
+
provider: null,
|
|
128
|
+
providerVersion: null,
|
|
129
|
+
stubs,
|
|
130
|
+
baseDir: configDir,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ORDER: Record<StubOrigin, number> = { library: 0, 'stub-package': 1, config: 2 }
|
|
135
|
+
sources.sort((a, b) => ORDER[a.origin] - ORDER[b.origin])
|
|
136
|
+
const nativeModules: Record<string, NativeModuleResolution> = {}
|
|
137
|
+
const nativeViews: Record<string, NativeViewResolution> = {}
|
|
138
|
+
const applied: AppliedStubs[] = []
|
|
139
|
+
for (const source of sources) {
|
|
140
|
+
const { modules, views } = expandStubs(appDir, source)
|
|
141
|
+
for (const [name, stub] of Object.entries(modules))
|
|
142
|
+
nativeModules[name] = lowerStub(stub, source.baseDir)
|
|
143
|
+
for (const [name, stub] of Object.entries(views))
|
|
144
|
+
nativeViews[name] = lowerStub(stub, source.baseDir)
|
|
145
|
+
applied.push({
|
|
146
|
+
target: source.target,
|
|
147
|
+
origin: source.origin,
|
|
148
|
+
provider: source.provider,
|
|
149
|
+
providerVersion: source.providerVersion,
|
|
150
|
+
modules: Object.keys(modules),
|
|
151
|
+
views: Object.keys(views),
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
const stubs = { applied, skipped }
|
|
155
|
+
if (!config && applied.length === 0) return { config, stubs }
|
|
156
|
+
const lowered: PeachConfig = { ...config, nativeModules, nativeViews }
|
|
157
|
+
delete lowered.packages
|
|
158
|
+
return { config: lowered, stubs }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// a whole-package 'noop' answers every native name the installed package asks for
|
|
162
|
+
function expandStubs(
|
|
163
|
+
appDir: string,
|
|
164
|
+
source: StubSource,
|
|
165
|
+
): { modules: Record<string, NativeStub>; views: Record<string, NativeStub> } {
|
|
166
|
+
if (source.stubs !== 'noop')
|
|
167
|
+
return { modules: source.stubs.modules ?? {}, views: source.stubs.views ?? {} }
|
|
168
|
+
const dir = installedPackageDir(appDir, source.target)
|
|
169
|
+
if (!dir)
|
|
170
|
+
throw new Error(
|
|
171
|
+
`packages['${source.target}']: 'noop' answers the names an installed package asks for, and ${source.target} is not installed`,
|
|
172
|
+
)
|
|
173
|
+
const modules: Record<string, NativeStub> = {}
|
|
174
|
+
const views: Record<string, NativeStub> = {}
|
|
175
|
+
for (const request of packageNativeRequests(dir)) {
|
|
176
|
+
if (request.kind === 'module') modules[request.name] = 'noop'
|
|
177
|
+
else views[request.name] = 'noop'
|
|
178
|
+
}
|
|
179
|
+
return { modules, views }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function lowerStub(stub: NativeStub, baseDir: string): 'noop' | { file: string } {
|
|
183
|
+
return stub === 'noop' ? stub : { file: resolve(baseDir, stub) }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function readPackageJson(dir: string): Record<string, unknown> | null {
|
|
187
|
+
const text = readSmallFile(join(dir, 'package.json'))
|
|
188
|
+
if (!text) return null
|
|
189
|
+
try {
|
|
190
|
+
const parsed: unknown = JSON.parse(text)
|
|
191
|
+
return isRecord(parsed) ? parsed : null
|
|
192
|
+
} catch {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// the installed copy of a dependency, found the way node resolves it
|
|
198
|
+
export function installedPackageDir(appDir: string, name: string): string | null {
|
|
199
|
+
let current = appDir
|
|
200
|
+
while (true) {
|
|
201
|
+
const dir = join(current, 'node_modules', name)
|
|
202
|
+
if (existsSync(join(dir, 'package.json'))) return realpathSync(dir)
|
|
203
|
+
const parent = dirname(current)
|
|
204
|
+
if (parent === current) return null
|
|
205
|
+
current = parent
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface NativeRequest {
|
|
210
|
+
kind: 'module' | 'view'
|
|
211
|
+
name: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const SOURCE_FILE = /\.[cm]?[jt]sx?$/
|
|
215
|
+
export const NATIVE_LOOKUP = /requireNative|TurboModuleRegistry|codegenNativeComponent/
|
|
216
|
+
// a requireNativeView(module, view) name is `module_view`, the key the engine
|
|
217
|
+
// registers expo views under.
|
|
218
|
+
export const LOOKUPS: { kind: NativeRequest['kind']; pattern: RegExp }[] = [
|
|
219
|
+
{
|
|
220
|
+
kind: 'view',
|
|
221
|
+
pattern: /\brequireNativeComponent\s*(?:<[^;]{0,200}?>)?\(\s*['"]([\w$.-]+)['"]/g,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
kind: 'view',
|
|
225
|
+
pattern:
|
|
226
|
+
/\brequireNativeView(?:Manager)?\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]\s*(?:,\s*['"](\w+)['"])?/g,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
kind: 'view',
|
|
230
|
+
pattern: /\bcodegenNativeComponent\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]/g,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
kind: 'module',
|
|
234
|
+
pattern:
|
|
235
|
+
/\brequire(?:Optional)?NativeModule\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]/g,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
kind: 'module',
|
|
239
|
+
pattern:
|
|
240
|
+
/\bTurboModuleRegistry\.get(?:Enforcing)?\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]/g,
|
|
241
|
+
},
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
// the native names a package's own JavaScript asks for, outside its native
|
|
245
|
+
// sources (top-level ios and android; src/ios is JavaScript), tests and examples
|
|
246
|
+
const NATIVE_SOURCE_DIRS = new Set(['ios', 'android'])
|
|
247
|
+
const PACKAGE_SKIP_DIRS = new Set([
|
|
248
|
+
'example',
|
|
249
|
+
'examples',
|
|
250
|
+
'__tests__',
|
|
251
|
+
'__mocks__',
|
|
252
|
+
'docs',
|
|
253
|
+
])
|
|
254
|
+
const LEGACY_NATIVE_MODULE = /\bNativeModules\s*\.\s*(\w+)/g
|
|
255
|
+
|
|
256
|
+
export function packageNativeRequests(dir: string): NativeRequest[] {
|
|
257
|
+
const found = new Map<string, NativeRequest>()
|
|
258
|
+
const directories = scanRepository(dir).directories.filter((sub) => {
|
|
259
|
+
const segments = relative(dir, sub).split('/')
|
|
260
|
+
return (
|
|
261
|
+
!NATIVE_SOURCE_DIRS.has(segments[0]) &&
|
|
262
|
+
!segments.some((segment) => PACKAGE_SKIP_DIRS.has(segment))
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
let files = 0
|
|
266
|
+
for (const sub of directories) {
|
|
267
|
+
for (const name of listDir(sub)) {
|
|
268
|
+
if (!SOURCE_FILE.test(name) || name.endsWith('.d.ts') || files >= 2_000) continue
|
|
269
|
+
files++
|
|
270
|
+
const text = readSmallFile(join(sub, name))
|
|
271
|
+
if (!text || !(NATIVE_LOOKUP.test(text) || text.includes('NativeModules'))) continue
|
|
272
|
+
for (const { kind, pattern } of [
|
|
273
|
+
...LOOKUPS,
|
|
274
|
+
{ kind: 'module' as const, pattern: LEGACY_NATIVE_MODULE },
|
|
275
|
+
]) {
|
|
276
|
+
for (const match of text.matchAll(pattern)) {
|
|
277
|
+
const request = match[2] ? `${match[1]}_${match[2]}` : match[1]
|
|
278
|
+
if (request) found.set(`${kind}:${request}`, { kind, name: request })
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return [...found.values()].sort(
|
|
284
|
+
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function listDir(dir: string): string[] {
|
|
289
|
+
try {
|
|
290
|
+
return readdirSync(dir)
|
|
291
|
+
} catch {
|
|
292
|
+
return []
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function readSmallFile(file: string): string | null {
|
|
297
|
+
try {
|
|
298
|
+
return statSync(file).size <= 1024 * 1024 ? readFileSync(file, 'utf8') : null
|
|
299
|
+
} catch {
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
}
|
package/cli/bin.ts
CHANGED
|
@@ -53,9 +53,13 @@ if (process.argv[2] === '__cloud-confirm') {
|
|
|
53
53
|
// work does. so this is the branch that must skip the CLI rather than the CLI
|
|
54
54
|
// that must skip it.
|
|
55
55
|
const forkedScript = process.argv[2]
|
|
56
|
-
if (
|
|
56
|
+
if (
|
|
57
|
+
process.env[RNX_FORKED_SCRIPT_ENV] === '1' &&
|
|
58
|
+
forkedScript &&
|
|
59
|
+
/\.(?:c|m)?js$/.test(forkedScript)
|
|
60
|
+
) {
|
|
57
61
|
const { runScript } = await import('./internal-child')
|
|
58
|
-
runScript(forkedScript, process.argv.slice(3))
|
|
62
|
+
await runScript(forkedScript, process.argv.slice(3))
|
|
59
63
|
} else if (process.argv[2] === RNX_INTERNAL_COMMAND) {
|
|
60
64
|
const { runInternalChild } = await import('./internal-child')
|
|
61
65
|
await runInternalChild(process.argv.slice(3))
|