peachsim 0.1.1625 → 0.1.1640
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 +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- 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 +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- package/detox/navigation-lifecycle.ts +35 -3
- 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 +8 -7
- 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 +11887 -7900
- 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 +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- 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 +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 +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- 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 +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/backend-origin.ts +13 -6
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
package/LICENSE
CHANGED
|
@@ -8,7 +8,7 @@ the Peach Software License Agreement: https://peachsim.com/legal/license
|
|
|
8
8
|
In summary (the agreement governs): Seed is free for individuals and companies
|
|
9
9
|
under 10 people and under $1M yearly revenue, including commercial desktop and
|
|
10
10
|
local use. A company at or above either limit needs Team while it has under
|
|
11
|
-
|
|
11
|
+
25 people and under $3M yearly revenue, or Enterprise at or above either of
|
|
12
12
|
those limits. Members, agents, API keys and local simulators are unlimited.
|
|
13
13
|
Company size is self-reported and enforced through the license terms only.
|
|
14
14
|
Embedding or redistributing the software requires a separate written agreement
|
package/README.md
CHANGED
|
@@ -323,7 +323,16 @@ resolve` re-pins it, and the CLI release publishes any bundle the bucket lacks.
|
|
|
323
323
|
On Mac, headless launches attach an installed compatible native host. With no
|
|
324
324
|
host, they render in the browser without downloading or placing the app. The
|
|
325
325
|
browser logo menu offers the app; `peach desktop install --native` remains the
|
|
326
|
-
explicit CLI installer. Linux
|
|
326
|
+
explicit CLI installer. Linux x64 headless opens acquire the native standalone
|
|
327
|
+
host under `~/.peach` by default. To choose plain Chromium for an open, use
|
|
328
|
+
`peach open <target> --driver playwright --headless --chromium`.
|
|
329
|
+
Pin the native host for reproducible Linux runs with
|
|
330
|
+
`PEACH_DESKTOP_VERSION=<build>.<attempt> peach open <target> --headless`.
|
|
331
|
+
A cached pin is used directly, even if another build is active; a missing pin
|
|
332
|
+
is downloaded exactly, and an unavailable or incompatible pin fails the open.
|
|
333
|
+
`peach list --drivers` shows installed hosts and each running sim's renderer
|
|
334
|
+
and native executable path. Renderer choices select a matching sim or launch
|
|
335
|
+
one; they never change a running sim's renderer.
|
|
327
336
|
|
|
328
337
|
Headed launches on Linux use full Chromium, whose extra libraries the bundle
|
|
329
338
|
also carries. Later opens reuse that sim and
|
|
@@ -590,7 +599,11 @@ foreground or background state. Both retain its browser session and app state.
|
|
|
590
599
|
`sourceApp` does not add fields to React Native's `{ url }` event. A
|
|
591
600
|
stopped guest rejects URL delivery. Explicit `newInstance: true`, deletion,
|
|
592
601
|
and video recording create a new browser context; cold launches still use the
|
|
593
|
-
supplied shell URL.
|
|
602
|
+
supplied shell URL. Process termination and `newInstance: true` preserve
|
|
603
|
+
cookies, local storage and IndexedDB for the next launch. `delete: true` and
|
|
604
|
+
driver cleanup clear that retained storage. Relaunch creates a fresh runtime,
|
|
605
|
+
so input focus and other in-memory component state do not survive termination.
|
|
606
|
+
When synchronization is enabled, URL delivery waits for
|
|
594
607
|
existing work before dispatch and for navigation and painting afterward.
|
|
595
608
|
|
|
596
609
|
Element taps finish viewport measurement before resolving the selected node's
|
package/cli/app-config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import { createJiti } from 'jiti'
|
|
3
4
|
import * as peachConfigModule from '../src/config'
|
|
@@ -80,18 +81,29 @@ async function loadConfigFile(
|
|
|
80
81
|
[`${rnxPublicBrand.packageName}/config`]: peachConfigModule,
|
|
81
82
|
},
|
|
82
83
|
})
|
|
83
|
-
const loaded: unknown =
|
|
84
|
+
const loaded: unknown = project.configPath.endsWith('.json')
|
|
85
|
+
? JSON.parse(readFileSync(project.configPath, 'utf8'))
|
|
86
|
+
: await jiti.import(project.configPath, { default: true })
|
|
84
87
|
if (!isPeachConfig(loaded)) {
|
|
85
88
|
throw new Error('the default export must be a peach config object')
|
|
86
89
|
}
|
|
87
90
|
const wire = WIRE_SLOTS.filter((slot) => loaded[slot] !== undefined)
|
|
88
91
|
if (wire.length > 0) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const slots: readonly string[] = WIRE_SLOTS
|
|
93
|
+
const kept = Object.entries(loaded)
|
|
94
|
+
.filter(([key, value]) => !slots.includes(key) && value !== undefined)
|
|
95
|
+
.map(([key]) => key)
|
|
96
|
+
const migration = wireSlotMigration(
|
|
97
|
+
project.appDir,
|
|
98
|
+
project.configPath,
|
|
99
|
+
{
|
|
100
|
+
modules: { ...loaded.turboModules, ...loaded.nativeModules },
|
|
101
|
+
views: { ...loaded.nativeViews },
|
|
102
|
+
},
|
|
103
|
+
kept,
|
|
104
|
+
)
|
|
93
105
|
throw new Error(
|
|
94
|
-
`${wire.join(', ')} moved under packages, keyed by the package whose native side they answer.
|
|
106
|
+
`${wire.join(', ')} moved under packages, keyed by the package whose native side they answer. ${kept.length ? 'remove them and add' : 'replace the config with'}:\n${migration}`,
|
|
95
107
|
)
|
|
96
108
|
}
|
|
97
109
|
return resolveConfigReplacementFiles(loaded, project.configPath)
|
package/cli/app-fonts.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
resolveAppFontsWire,
|
|
8
8
|
expoFontPathsFromPlugins,
|
|
9
9
|
nativeLinkedFontSpecsFromPaths,
|
|
10
|
-
metroFontSpecsFromPaths,
|
|
11
10
|
appFontAssetUrlsFromWire,
|
|
12
11
|
type ConfigPluginFontSpec,
|
|
13
12
|
type FontPlatform,
|
|
@@ -15,8 +14,10 @@ import {
|
|
|
15
14
|
import { resolveListeningPid, resolveProcessCwd } from '../scripts/dev-server-scanner'
|
|
16
15
|
import { loopbackOriginOf } from '../src/backend-origin'
|
|
17
16
|
import { MANIFEST_PROBE_TIMEOUT_MS } from '../src/dev-bundle-resolution'
|
|
17
|
+
import { parseExpoManifestBody } from '../src/expo-manifest-body'
|
|
18
18
|
import { stageHostFile } from '../src/host/staged-host-files'
|
|
19
19
|
import { parsePlistSource } from '../src/plist.ts'
|
|
20
|
+
import { listXcodeAppTargets, readXcodeAppTarget } from '../src/xcode-project'
|
|
20
21
|
import { resolveAppProject } from './app-project'
|
|
21
22
|
import {
|
|
22
23
|
materialSymbolFontPaths,
|
|
@@ -31,24 +32,22 @@ export type DiscoveredAppFontSpec = ConfigPluginFontSpec
|
|
|
31
32
|
|
|
32
33
|
type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
|
|
33
34
|
|
|
34
|
-
// every font file is found on disk, so
|
|
35
|
-
// never
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// busy the dev server is, and a cold build answers late enough to drop fonts.
|
|
35
|
+
// every font file is found on disk, so its bytes are staged as host files and
|
|
36
|
+
// never fetched from the dev server: only metro serves project files at
|
|
37
|
+
// `/assets/<path>`, and a reachability request would make the result depend on
|
|
38
|
+
// how busy the dev server is, so a cold build could answer late and drop fonts.
|
|
39
39
|
export function discoverNativeLinkedAppFonts(opts: {
|
|
40
|
-
bundleUrl: string
|
|
41
40
|
projectDir: string
|
|
42
41
|
platform?: string
|
|
43
42
|
nativeAssetOrigin: string
|
|
44
43
|
}): DiscoveredAppFontSpec[] {
|
|
45
|
-
const metroOrigin = loopbackOriginOf(opts.bundleUrl)
|
|
46
|
-
if (!metroOrigin) return []
|
|
47
44
|
const { paths, materialSymbols } = discoverNativeLinkedAppFontAssets(opts)
|
|
48
|
-
const
|
|
45
|
+
const realProjectRoot = fs.realpathSync(opts.projectDir)
|
|
46
|
+
const specs = nativeLinkedFontSpecsFromPaths(
|
|
49
47
|
projectRelativeFontPaths(opts.projectDir, paths),
|
|
50
|
-
metroOrigin,
|
|
51
48
|
nativeFontPlatform(opts.platform),
|
|
49
|
+
(fontPath) =>
|
|
50
|
+
stageHostFile(path.resolve(realProjectRoot, fontPath), opts.nativeAssetOrigin),
|
|
52
51
|
)
|
|
53
52
|
attachMaterialSymbolMetadata(specs, materialSymbols, (asset) =>
|
|
54
53
|
stageHostFile(asset, opts.nativeAssetOrigin),
|
|
@@ -286,6 +285,23 @@ function discoverNativeLinkedAppFontAssets(opts: {
|
|
|
286
285
|
!entry.name.startsWith('.'),
|
|
287
286
|
)
|
|
288
287
|
.slice(0, 64)
|
|
288
|
+
// a bare filename in UIAppFonts names a file the target copies into
|
|
289
|
+
// its bundle, wherever the project keeps it: resolve it by the target's
|
|
290
|
+
// resource build phase before trying paths beside the plist.
|
|
291
|
+
const bundledByName = new Map<string, string>()
|
|
292
|
+
for (const entry of fs.readdirSync(iosDir, { withFileTypes: true })) {
|
|
293
|
+
if (!entry.isDirectory() || !entry.name.endsWith('.xcodeproj')) continue
|
|
294
|
+
const xcodeproj = path.join(iosDir, entry.name)
|
|
295
|
+
for (const target of listXcodeAppTargets(xcodeproj, { requireSources: false })) {
|
|
296
|
+
const members = readXcodeAppTarget(xcodeproj, target, undefined, {
|
|
297
|
+
requireSources: false,
|
|
298
|
+
})
|
|
299
|
+
for (const file of members.resources) {
|
|
300
|
+
if (!bundledByName.has(path.basename(file)))
|
|
301
|
+
bundledByName.set(path.basename(file), file)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
289
305
|
const plistPaths = [path.join(iosDir, 'Info.plist')]
|
|
290
306
|
for (const entry of targetDirs) {
|
|
291
307
|
plistPaths.push(path.join(iosDir, entry.name, 'Info.plist'))
|
|
@@ -302,15 +318,19 @@ function discoverNativeLinkedAppFontAssets(opts: {
|
|
|
302
318
|
if (!Array.isArray(declaredFonts)) continue
|
|
303
319
|
for (const declaredFont of declaredFonts) {
|
|
304
320
|
if (typeof declaredFont !== 'string') continue
|
|
321
|
+
// a file the target copies into its bundle is linked wherever it
|
|
322
|
+
// lives, including a workspace package's folder outside the app.
|
|
323
|
+
const bundled = bundledByName.get(declaredFont)
|
|
305
324
|
const candidates = [
|
|
306
325
|
path.resolve(path.dirname(plistPath), declaredFont),
|
|
307
326
|
path.resolve(iosDir, declaredFont),
|
|
308
327
|
]
|
|
309
|
-
const fontPath =
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
328
|
+
const fontPath =
|
|
329
|
+
candidates.find((candidate) => {
|
|
330
|
+
const relative = path.relative(selectedAppDir, candidate)
|
|
331
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return false
|
|
332
|
+
return isFile(candidate)
|
|
333
|
+
}) ?? (bundled && isFile(bundled) ? bundled : undefined)
|
|
314
334
|
if (fontPath) fontPaths.push(fontPath)
|
|
315
335
|
}
|
|
316
336
|
}
|
|
@@ -383,7 +403,7 @@ export async function resolveMetroProjectRoot(bundleUrl: string): Promise<string
|
|
|
383
403
|
signal: controller.signal,
|
|
384
404
|
})
|
|
385
405
|
if (res.ok) {
|
|
386
|
-
const manifest = (await res.
|
|
406
|
+
const manifest = parseExpoManifestBody(await res.text()) as {
|
|
387
407
|
extra?: { expoClient?: { _internal?: { projectRoot?: unknown } } }
|
|
388
408
|
}
|
|
389
409
|
servedManifest = true
|
package/cli/app-stubs.ts
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
// packages, and a library's own `peach` field, lowered into the engine's
|
|
4
4
|
// nativeModules / nativeViews wire slots.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
statSync,
|
|
13
|
+
} from 'node:fs'
|
|
7
14
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
8
15
|
import { resolveCompatVersionRange } from '@peach/compat/version-range'
|
|
9
16
|
import { parseSync, type Expression } from 'oxc-parser'
|
|
@@ -18,8 +25,16 @@ import {
|
|
|
18
25
|
type PeachConfig,
|
|
19
26
|
} from '../src/config'
|
|
20
27
|
import { isRecord } from '../src/is-record'
|
|
28
|
+
import { rnxPublicBrand } from '../src/public-brand'
|
|
21
29
|
import { visitParsedProgram } from '../src/visit-parsed-program'
|
|
22
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
readProjectManifest,
|
|
32
|
+
scanRepository,
|
|
33
|
+
metadataValue,
|
|
34
|
+
readProjectJson,
|
|
35
|
+
} from './commands/detect'
|
|
36
|
+
import type { ProjectManifest } from './commands/projectReportContract'
|
|
37
|
+
import type { ScanResult } from '@peach/compat/web'
|
|
23
38
|
|
|
24
39
|
// who answers a package, in rising precedence
|
|
25
40
|
export type StubOrigin = 'library' | 'stub-package' | 'config'
|
|
@@ -125,7 +140,25 @@ export function resolveAppStubs(
|
|
|
125
140
|
baseDir: dir,
|
|
126
141
|
})
|
|
127
142
|
}
|
|
128
|
-
|
|
143
|
+
const configuredPackages = Object.entries(config?.packages ?? {})
|
|
144
|
+
const packageNames = new Set<string>()
|
|
145
|
+
if (typeof manifest?.name === 'string') packageNames.add(manifest.name)
|
|
146
|
+
for (const [target] of configuredPackages) {
|
|
147
|
+
const dir = installedPackageDir(appDir, target)
|
|
148
|
+
if (dir && readPackageJson(dir)?.name === target) packageNames.add(target)
|
|
149
|
+
}
|
|
150
|
+
if (manifest && configuredPackages.some(([target]) => !packageNames.has(target))) {
|
|
151
|
+
for (const root of [appDir, ...workspacePackageDirs(appDir, manifest)])
|
|
152
|
+
for (const dir of scanRepository(root).directories) {
|
|
153
|
+
const name = readPackageJson(dir)?.name
|
|
154
|
+
if (typeof name === 'string') packageNames.add(name)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const [target, stubs] of configuredPackages) {
|
|
158
|
+
if (manifest && !packageNames.has(target))
|
|
159
|
+
throw new Error(
|
|
160
|
+
`packages[${JSON.stringify(target)}]: no installed or local package has this name; use the name from the package's package.json`,
|
|
161
|
+
)
|
|
129
162
|
sources.push({
|
|
130
163
|
target,
|
|
131
164
|
origin: 'config',
|
|
@@ -348,32 +381,73 @@ export function packageNativeRequests(dir: string): NativeRequest[] {
|
|
|
348
381
|
)
|
|
349
382
|
}
|
|
350
383
|
|
|
351
|
-
// the
|
|
352
|
-
// name
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
export function wireSlotMigration(
|
|
384
|
+
// the native names each installed dependency's own JavaScript asks for, keyed
|
|
385
|
+
// `kind:name`. links to source outside node_modules are the app's own packages
|
|
386
|
+
// and are scanned as app code instead.
|
|
387
|
+
function dependencyNativeOwners(
|
|
356
388
|
appDir: string,
|
|
357
|
-
|
|
358
|
-
): string {
|
|
389
|
+
manifest: ProjectManifest | null,
|
|
390
|
+
): Map<string, string> {
|
|
359
391
|
const owners = new Map<string, string>()
|
|
360
|
-
const manifest = readProjectManifest(appDir)
|
|
361
392
|
for (const name of Object.keys({
|
|
362
393
|
...manifest?.devDependencies,
|
|
363
394
|
...manifest?.dependencies,
|
|
364
395
|
})) {
|
|
365
396
|
const dir = installedPackageDir(appDir, name)
|
|
366
|
-
if (!dir) continue
|
|
397
|
+
if (!dir || !dir.split('/').includes('node_modules')) continue
|
|
367
398
|
for (const request of packageNativeRequests(dir))
|
|
368
399
|
if (!owners.has(`${request.kind}:${request.name}`))
|
|
369
400
|
owners.set(`${request.kind}:${request.name}`, name)
|
|
370
401
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
402
|
+
return owners
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// a complete config module in the form the app's config file already uses, so
|
|
406
|
+
// the printed text pastes over it
|
|
407
|
+
function configModule(configPath: string | null, packages: string[]): string {
|
|
408
|
+
const body = [' packages: {', ...packages, ' },']
|
|
409
|
+
if (
|
|
410
|
+
configPath &&
|
|
411
|
+
(configPath.endsWith('.cjs') ||
|
|
412
|
+
(configPath.endsWith('.js') &&
|
|
413
|
+
/\bmodule\.exports\b/.test(readSmallFile(configPath) ?? '')))
|
|
414
|
+
)
|
|
415
|
+
return ['module.exports = {', ...body, '}'].join('\n')
|
|
416
|
+
return [
|
|
417
|
+
`import { defineConfig } from '${rnxPublicBrand.packageName}/config'`,
|
|
418
|
+
'',
|
|
419
|
+
'export default defineConfig({',
|
|
420
|
+
...body,
|
|
421
|
+
'})',
|
|
422
|
+
].join('\n')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// what replaces a config's old top-level wire slots: each name keyed by the
|
|
426
|
+
// package whose JavaScript asks for it. a config holding only those slots gets
|
|
427
|
+
// a complete module; one with other settings (`kept`) gets just the packages
|
|
428
|
+
// entries, since a whole module would drop them. `packages` takes only 'noop'
|
|
429
|
+
// or a file path, so any other old value is named for a hand move instead of
|
|
430
|
+
// guessed.
|
|
431
|
+
export function wireSlotMigration(
|
|
432
|
+
appDir: string,
|
|
433
|
+
configPath: string | null,
|
|
434
|
+
wire: { modules: Record<string, unknown>; views: Record<string, unknown> },
|
|
435
|
+
kept: readonly string[] = [],
|
|
436
|
+
): string {
|
|
437
|
+
const manifest = readProjectManifest(appDir)
|
|
438
|
+
const owners = dependencyNativeOwners(appDir, manifest)
|
|
439
|
+
if (manifest)
|
|
440
|
+
for (const request of scanLocalNative(
|
|
441
|
+
appDir,
|
|
442
|
+
manifest,
|
|
443
|
+
{ module: new Set(), view: new Set() },
|
|
444
|
+
undefined,
|
|
445
|
+
new Set(),
|
|
446
|
+
true,
|
|
447
|
+
owners,
|
|
448
|
+
))
|
|
374
449
|
if (!owners.has(`${request.kind}:${request.name}`))
|
|
375
|
-
owners.set(`${request.kind}:${request.name}`,
|
|
376
|
-
const unknownOwner = '<package that asks for it>'
|
|
450
|
+
owners.set(`${request.kind}:${request.name}`, request.package)
|
|
377
451
|
const blocks = new Map<string, { modules: string[]; views: string[] }>()
|
|
378
452
|
const manual: string[] = []
|
|
379
453
|
for (const [kind, entries] of [
|
|
@@ -395,7 +469,13 @@ export function wireSlotMigration(
|
|
|
395
469
|
)
|
|
396
470
|
continue
|
|
397
471
|
}
|
|
398
|
-
const owner = owners.get(`${kind}:${name}`)
|
|
472
|
+
const owner = owners.get(`${kind}:${name}`)
|
|
473
|
+
if (!owner) {
|
|
474
|
+
manual.push(
|
|
475
|
+
`${name}: no requesting package was found; find the package that requests this ${kind} before adding its packages entry`,
|
|
476
|
+
)
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
399
479
|
const block = blocks.get(owner) ?? { modules: [], views: [] }
|
|
400
480
|
block[kind === 'module' ? 'modules' : 'views'].push(
|
|
401
481
|
`[${JSON.stringify(name)}]: ${stub}`,
|
|
@@ -410,9 +490,17 @@ export function wireSlotMigration(
|
|
|
410
490
|
].filter(Boolean)
|
|
411
491
|
return ` [${JSON.stringify(owner)}]: { ${parts.join(', ')} },`
|
|
412
492
|
})
|
|
493
|
+
if (lines.length && kept.length)
|
|
494
|
+
return [
|
|
495
|
+
...manual.map((line) => `// ${line}`),
|
|
496
|
+
`// keep ${kept.join(', ')}; merge these into packages`,
|
|
497
|
+
' packages: {',
|
|
498
|
+
...lines,
|
|
499
|
+
' },',
|
|
500
|
+
].join('\n')
|
|
413
501
|
return [
|
|
414
|
-
...(
|
|
415
|
-
...
|
|
502
|
+
...manual.map((line) => `// ${line}`),
|
|
503
|
+
...(lines.length ? [configModule(configPath, lines)] : []),
|
|
416
504
|
].join('\n')
|
|
417
505
|
}
|
|
418
506
|
|
|
@@ -431,3 +519,179 @@ export function readSmallFile(file: string): string | null {
|
|
|
431
519
|
return null
|
|
432
520
|
}
|
|
433
521
|
}
|
|
522
|
+
|
|
523
|
+
export function hasNativeCode(
|
|
524
|
+
dir: string,
|
|
525
|
+
platform: ScanResult['scoreSummary']['platform'],
|
|
526
|
+
): boolean {
|
|
527
|
+
const expoConfig = join(dir, 'expo-module.config.json')
|
|
528
|
+
const platforms = metadataValue(readProjectJson(expoConfig), 'platforms')
|
|
529
|
+
if (Array.isArray(platforms))
|
|
530
|
+
return (
|
|
531
|
+
platforms.includes(platform) || (platform === 'ios' && platforms.includes('apple'))
|
|
532
|
+
)
|
|
533
|
+
if ([platform, 'expo-module.config.json'].some((entry) => existsSync(join(dir, entry))))
|
|
534
|
+
return true
|
|
535
|
+
if (listDir(dir).some((name) => name.endsWith('.podspec'))) return true
|
|
536
|
+
const manifest = readSmallFile(join(dir, 'package.json'))
|
|
537
|
+
return manifest !== null && manifest.includes('"codegenConfig"')
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// an app's own native code, found from the names its JavaScript asks the native
|
|
541
|
+
// side for and from local Expo modules (a directory with expo-module.config.json).
|
|
542
|
+
// each name is a key under its package in peach.config.ts `packages`.
|
|
543
|
+
export interface LocalNative {
|
|
544
|
+
kind: 'module' | 'view'
|
|
545
|
+
name: string
|
|
546
|
+
// the package the name belongs to: the nearest package.json with a name
|
|
547
|
+
package: string
|
|
548
|
+
// first place the name appears, relative to the app, as file:line
|
|
549
|
+
path: string
|
|
550
|
+
// the local Expo module directory the name belongs to, relative to the app
|
|
551
|
+
expoModule: string | null
|
|
552
|
+
swift: boolean
|
|
553
|
+
// a stub already answers it
|
|
554
|
+
answered: boolean
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const NATIVE_SOURCE_FILE = /\.(?:swift|kt)$/
|
|
558
|
+
export type AnsweredNames = Record<LocalNative['kind'], Set<string>>
|
|
559
|
+
|
|
560
|
+
export function scanLocalNative(
|
|
561
|
+
appDir: string,
|
|
562
|
+
pkg: ProjectManifest,
|
|
563
|
+
answered: AnsweredNames,
|
|
564
|
+
platform: ScanResult['scoreSummary']['platform'] | undefined,
|
|
565
|
+
knownPackages: ReadonlySet<string> = new Set(),
|
|
566
|
+
includeLegacy = false,
|
|
567
|
+
providers?: ReadonlyMap<string, string>,
|
|
568
|
+
): LocalNative[] {
|
|
569
|
+
const expoModuleDirs: string[] = []
|
|
570
|
+
const excludedModuleDirs: string[] = []
|
|
571
|
+
const sourceFiles: string[] = []
|
|
572
|
+
for (const root of [appDir, ...workspacePackageDirs(appDir, pkg)]) {
|
|
573
|
+
for (const dir of scanRepository(root).directories) {
|
|
574
|
+
if (
|
|
575
|
+
excludedModuleDirs.some(
|
|
576
|
+
(excluded) => dir === excluded || dir.startsWith(`${excluded}/`),
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
continue
|
|
580
|
+
if (existsSync(join(dir, 'expo-module.config.json'))) {
|
|
581
|
+
if (platform && !hasNativeCode(dir, platform)) {
|
|
582
|
+
excludedModuleDirs.push(dir)
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
expoModuleDirs.push(dir)
|
|
586
|
+
}
|
|
587
|
+
for (const name of listDir(dir)) {
|
|
588
|
+
if (SOURCE_FILE.test(name) && !name.endsWith('.d.ts'))
|
|
589
|
+
sourceFiles.push(join(dir, name))
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const nativeFilesByModule = new Map(
|
|
594
|
+
expoModuleDirs.map((dir) => [
|
|
595
|
+
dir,
|
|
596
|
+
scanRepository(dir).directories.flatMap((sub) =>
|
|
597
|
+
listDir(sub)
|
|
598
|
+
.filter((name) => NATIVE_SOURCE_FILE.test(name))
|
|
599
|
+
.map((name) => join(sub, name)),
|
|
600
|
+
),
|
|
601
|
+
]),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
const owners = new Map<string, string>()
|
|
605
|
+
const owner = (dir: string): string => {
|
|
606
|
+
const known = owners.get(dir)
|
|
607
|
+
if (known) return known
|
|
608
|
+
const name = readPackageJson(dir)?.name
|
|
609
|
+
const parent = dirname(dir)
|
|
610
|
+
const result =
|
|
611
|
+
typeof name === 'string'
|
|
612
|
+
? name
|
|
613
|
+
: parent === dir
|
|
614
|
+
? (pkg.name ?? 'app')
|
|
615
|
+
: owner(parent)
|
|
616
|
+
owners.set(dir, result)
|
|
617
|
+
return result
|
|
618
|
+
}
|
|
619
|
+
const found = new Map<string, LocalNative>()
|
|
620
|
+
// a name an installed dependency asks for belongs to that dependency, whoever
|
|
621
|
+
// in the app spells it out
|
|
622
|
+
const add = (kind: LocalNative['kind'], name: string, file: string, line: number) => {
|
|
623
|
+
const key = `${kind}:${name}`
|
|
624
|
+
if (found.has(key)) return
|
|
625
|
+
providers ??= dependencyNativeOwners(appDir, pkg)
|
|
626
|
+
const packageName = providers.get(key) ?? owner(dirname(file))
|
|
627
|
+
if (knownPackages.has(packageName)) return
|
|
628
|
+
const moduleDir = expoModuleDirs.find((dir) => file.startsWith(`${dir}/`)) ?? null
|
|
629
|
+
found.set(key, {
|
|
630
|
+
kind,
|
|
631
|
+
name,
|
|
632
|
+
package: packageName,
|
|
633
|
+
path: `${relative(appDir, file)}:${line}`,
|
|
634
|
+
expoModule: moduleDir ? relative(appDir, moduleDir) || '.' : null,
|
|
635
|
+
swift: moduleDir
|
|
636
|
+
? (nativeFilesByModule.get(moduleDir) ?? []).some((path) =>
|
|
637
|
+
path.endsWith('.swift'),
|
|
638
|
+
)
|
|
639
|
+
: false,
|
|
640
|
+
answered: answered[kind].has(name),
|
|
641
|
+
})
|
|
642
|
+
}
|
|
643
|
+
for (const file of sourceFiles) {
|
|
644
|
+
const text = readSmallFile(file)
|
|
645
|
+
if (!text) continue
|
|
646
|
+
for (const { kind, name, line } of sourceNativeRequests(file, text, includeLegacy))
|
|
647
|
+
add(kind, name, file, line)
|
|
648
|
+
}
|
|
649
|
+
// a local expo module declares its name in its native definition, Name("X"),
|
|
650
|
+
// whether or not the app's javascript spells it out. swift first, so a
|
|
651
|
+
// module the report marks Swift points at its swift definition.
|
|
652
|
+
for (const [, files] of nativeFilesByModule) {
|
|
653
|
+
const swiftFirst = [...files].sort(
|
|
654
|
+
(a, b) => Number(b.endsWith('.swift')) - Number(a.endsWith('.swift')),
|
|
655
|
+
)
|
|
656
|
+
for (const file of swiftFirst) {
|
|
657
|
+
const text = readSmallFile(file)
|
|
658
|
+
if (!text) continue
|
|
659
|
+
for (const match of text.matchAll(/\bName\(\s*"(\w+)"\s*\)/g)) {
|
|
660
|
+
if (match[1])
|
|
661
|
+
add('module', match[1], file, text.slice(0, match.index).split('\n').length)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return [...found.values()].sort(
|
|
666
|
+
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// dependencies installed as links to source outside any node_modules are the
|
|
671
|
+
// app's own workspace packages, which hold its native code as often as the app
|
|
672
|
+
// directory does.
|
|
673
|
+
function workspacePackageDirs(appDir: string, pkg: ProjectManifest): string[] {
|
|
674
|
+
const dirs = new Set<string>()
|
|
675
|
+
for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
676
|
+
let current = appDir
|
|
677
|
+
while (true) {
|
|
678
|
+
const link = join(current, 'node_modules', name)
|
|
679
|
+
if (existsSync(link)) {
|
|
680
|
+
if (lstatSync(link).isSymbolicLink()) {
|
|
681
|
+
const target = realpathSync(link)
|
|
682
|
+
if (
|
|
683
|
+
!target.split('/').includes('node_modules') &&
|
|
684
|
+
!target.startsWith(`${appDir}/`)
|
|
685
|
+
) {
|
|
686
|
+
dirs.add(target)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
break
|
|
690
|
+
}
|
|
691
|
+
const parent = dirname(current)
|
|
692
|
+
if (parent === current) break
|
|
693
|
+
current = parent
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return [...dirs]
|
|
697
|
+
}
|