peachsim 0.1.1594 → 0.1.1619
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 +34 -11
- package/cli/app-catalog.ts +7 -5
- package/cli/app-config.ts +6 -2
- package/cli/app-fonts.ts +74 -35
- package/cli/app-stubs.ts +173 -42
- package/cli/bin.ts +9 -0
- package/cli/bridge-flow-runner.ts +189 -84
- package/cli/cloud-client.ts +26 -25
- package/cli/commands/agent-indicator-mode.ts +39 -0
- package/cli/commands/box.ts +13 -3
- package/cli/commands/control.ts +132 -108
- package/cli/commands/daemon.ts +99 -24
- package/cli/commands/debug.ts +29 -1
- package/cli/commands/detect.ts +1 -1
- package/cli/commands/device.ts +6 -8
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/doctor.ts +97 -34
- package/cli/commands/flow.ts +22 -12
- package/cli/commands/inspect/actions.ts +2 -0
- package/cli/commands/inspect/core.ts +106 -40
- package/cli/commands/inspect/list.ts +9 -3
- package/cli/commands/inspect/memory.ts +9 -0
- package/cli/commands/inspect/settling.ts +167 -49
- package/cli/commands/inspect/shared.ts +53 -12
- package/cli/commands/inspect/wait-event.ts +0 -1
- package/cli/commands/inspect/wait.ts +2 -2
- package/cli/commands/inspect.ts +19 -4
- package/cli/commands/kotlin.ts +5 -7
- package/cli/commands/maestro-compare.ts +25 -2
- package/cli/commands/mode.ts +13 -1
- package/cli/commands/perf-record.ts +296 -0
- package/cli/commands/perf.ts +6 -1
- package/cli/commands/platform.ts +5 -4
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +28 -5
- package/cli/commands/runtime.ts +9 -11
- package/cli/commands/screenshots.ts +4 -3
- package/cli/commands/setup.ts +17 -5
- package/cli/commands/skills.ts +16 -43
- package/cli/commands/swift.ts +18 -3
- package/cli/commands/upgrade.ts +7 -6
- package/cli/commands/upload.ts +27 -51
- package/cli/commands/window.ts +449 -0
- package/cli/desktop-companion.ts +84 -10
- package/cli/drivers/desktop.ts +2 -0
- package/cli/flow-file.ts +1 -0
- package/cli/maestro-compare.ts +19 -3
- package/cli/main.ts +22 -1
- package/cli/material-symbol-fonts.ts +171 -0
- package/cli/outbound-endpoints.ts +27 -13
- package/cli/preview-capture.ts +2 -2
- package/cli/run-registry.ts +5 -2
- package/cli/telemetry.ts +6 -3
- package/cli/ws-bridge.ts +15 -19
- package/detox/attempt-events.cjs +63 -7
- package/detox/attempt-events.d.cts +7 -0
- package/detox/gestures.ts +115 -2
- package/detox/index.ts +313 -411
- package/detox/jest-environment.cjs +86 -0
- package/detox/jest-preset.cjs +3 -1
- package/detox/jest-setup-after-env.cjs +1 -0
- package/detox/matchers.ts +3 -6
- package/detox/motion-change.cjs +9 -1
- package/detox/proof-frame.cjs +83 -9
- 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 +15 -5
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +4000 -1362
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +6 -1
- package/dist-lib/cloud-contract.mjs +5 -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 +26 -7
- package/dist-lib/config.mjs +26 -7
- package/dist-lib/detox/index.cjs +1908 -539
- package/dist-lib/detox/jest-environment.cjs +86 -0
- package/dist-lib/detox/jest-preset.cjs +3 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +3612 -1016
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +4755 -1825
- package/dist-lib/host/fetch-proxy-handler.cjs +59 -9
- package/dist-lib/host/fetch-proxy-overrides.cjs +62 -2
- package/dist-lib/host/fetch-proxy-overrides.mjs +56 -2
- package/dist-lib/host/replacement-module-handler.cjs +8 -8
- package/dist-lib/host/websocket-proxy.cjs +22 -6
- package/dist-lib/index.cjs +603 -184
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +33 -4
- package/dist-lib/kotlin-host.cjs +262 -121
- 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 +101 -7
- package/dist-lib/metro-production-bundle.mjs +106 -7
- package/dist-lib/metro.cjs +308 -87
- package/dist-lib/native-app-identity-project.cjs +99 -26
- package/dist-lib/native-app-identity-project.mjs +99 -26
- package/dist-lib/native-app-identity.cjs +25 -7
- package/dist-lib/native-app-identity.mjs +25 -7
- 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 +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +30 -9
- package/dist-lib/sdk.cjs +236 -78
- package/dist-lib/sdk.mjs +236 -78
- package/dist-lib/skills.cjs +6713 -2597
- package/dist-lib/swift-compile.cjs +1632 -299
- package/dist-lib/swift-compile.mjs +1553 -219
- package/dist-lib/swift-package.cjs +25 -3
- package/dist-lib/swift-package.mjs +22 -2
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +3670 -1051
- package/dist-lib/vite.cjs +142 -20
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/install.sh +1 -1
- package/kotlin/AppSources/android/view/Window.kt +4 -1
- package/kotlin/AppSources/android/window/BackEvent.kt +24 -0
- package/kotlin/AppSources/android/window/OnBackAnimationCallback.kt +7 -0
- package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +37 -1
- package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +5 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +8 -1
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +14 -0
- package/kotlin/CompilerSources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +1 -0
- package/kotlin/CompilerSources/PeachJniIncoming.kt +243 -0
- package/kotlin/CompilerSources/PeachJniPlugin.kt +116 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +27 -20
- package/kotlin/JniPlatformSources/androidx/annotation/CallSuper.kt +5 -0
- package/kotlin/JniPlatformSources/com/facebook/proguard/annotations/DoNotStrip.kt +5 -0
- package/kotlin/JniPlatformSources/dalvik/annotation/optimization/FastNative.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/Synchronized.kt +6 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmField.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmStatic.kt +5 -0
- package/kotlin/JniSources/com/facebook/jni/HybridData.kt +68 -0
- package/kotlin/JniSources/dev/peach/jni/IncomingBindings.kt +157 -0
- package/kotlin/JniSources/dev/peach/jni/NativeBindings.kt +79 -0
- package/kotlin/JniTransportSources/dev/peach/jni/integration/JniTransport.kt +91 -0
- package/kotlin/README.md +18 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +30 -40
- package/skills/contrast/SKILL.md +1 -1
- package/skills/peach-perf/SKILL.md +25 -0
- package/skills/peach-setup/SKILL.md +25 -1
- package/src/assetCatalogContract.ts +31 -3
- package/src/auth/shared-session.ts +3 -2
- package/src/bridge-contract.ts +5 -0
- package/src/bundle-channel.mjs +1 -1
- package/src/cloud-contract.ts +11 -0
- package/src/config.ts +16 -8
- package/src/expo-kotlin-host.ts +7 -2
- package/src/guest-bundle.ts +15 -8
- package/src/host/agent-host.ts +2 -4
- package/src/host/bridge-host.ts +52 -54
- package/src/host/fetch-proxy-handler.ts +94 -31
- package/src/host/fetch-proxy-overrides.ts +91 -0
- package/src/host/replacement-module-handler.ts +11 -10
- package/src/host/server-scan-handler.ts +1 -1
- package/src/host/staged-host-files.ts +92 -0
- package/src/host/websocket-proxy.ts +27 -6
- package/src/installed-package-versions.ts +1 -0
- package/src/jump-to-source-native.ts +3 -3
- package/src/kotlin-host.ts +143 -26
- package/src/kotlin-module.ts +52 -0
- package/src/metro-layout-parser.ts +78 -0
- package/src/metro-production-bundle.ts +59 -10
- package/src/native-app-identity-project.ts +65 -3
- package/src/native-app-identity.ts +31 -0
- package/src/native-firebase-config.ts +3 -2
- package/src/native-seam-manifest.ts +0 -44
- package/src/nativeAppIconContract.ts +11 -6
- package/src/nativeViewSourceContract.ts +49 -0
- package/src/nativeViewWebContract.ts +41 -0
- package/src/nitro-source.ts +3 -7
- package/src/one-widget-metadata.ts +23 -3
- package/src/oneWidgetContract.ts +22 -0
- package/src/process-group.ts +306 -0
- package/src/process-liveness.ts +24 -0
- package/src/process-output.ts +21 -2
- package/src/serverDiscoveryContract.ts +41 -0
- package/src/swift-compile.ts +1057 -265
- package/src/swift-design-time.ts +552 -0
- package/src/swift-distribution.lock.json +12 -11
- package/src/swift-host.ts +133 -25
- package/src/swift-objc-sdk.ts +726 -0
- package/src/swift-objc-standin.ts +441 -0
- package/src/swift-package.ts +25 -0
- package/src/swift-packages.ts +25 -10
- package/src/swift-standin-config.ts +94 -0
- package/src/swift-standin-emit.ts +5 -3
- package/src/swift-standins.ts +225 -81
- package/src/swiftContract.ts +4 -0
- package/src/target-matcher.ts +67 -0
- package/src/visit-parsed-program.ts +21 -0
- package/src/vite-plugin-one.ts +58 -16
- package/src/vite-plugin-swift.ts +1 -0
- package/src/vite-plugin.ts +105 -41
- package/src/worker-system-chunks.ts +86 -9
- package/src/worklets-babel.ts +85 -13
- package/src/xcode-project.ts +40 -15
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ drivers, and library exports; its only command, `npx peachsim`, runs that same
|
|
|
12
12
|
installer from the copy of `install.sh` it ships.
|
|
13
13
|
|
|
14
14
|
The executable updates itself with `peach upgrade`, which then restarts a running
|
|
15
|
-
background daemon onto the new executable
|
|
15
|
+
background daemon onto the new executable. A new runtime needs no restart: the
|
|
16
|
+
next new sim opens on it while open sims keep theirs. Successful interactive human
|
|
16
17
|
commands refresh a cached stable CLI version in a detached process at most once
|
|
17
18
|
every 20 hours. A newer version produces one short notice on stderr, once per
|
|
18
19
|
version and no more than weekly. CI, agents, JSON output, pipes, and
|
|
@@ -162,6 +163,11 @@ a local foreground bridge when no daemon is reachable, then opens a `WsBridge`
|
|
|
162
163
|
`cli/commands/inspect/*` and are re-exported as the programmatic `peachsim/sdk`;
|
|
163
164
|
the Maestro runner is `cli/bridge-flow-runner.ts`; the Detox compat driver
|
|
164
165
|
is in `detox/`. Test and screenshot paths additionally use `cli/drivers/*`.
|
|
166
|
+
Opening a simulator and waiting for reconnection share one app-tree readiness
|
|
167
|
+
poller. Its nominal wait budget includes WebSocket handshakes, target lookup,
|
|
168
|
+
evaluation and poll pauses; unanswered commands do not restart the deadline.
|
|
169
|
+
A timeout reports the last node count, missing test hook or bridge error and
|
|
170
|
+
elapsed time. Registering a simulator alone does not satisfy readiness.
|
|
165
171
|
Browser sims launch in an isolated Playwright Chrome for Testing profile.
|
|
166
172
|
An explicit `--base-url` keeps that shell's origin and uses its assets without
|
|
167
173
|
installing an active runtime. A bridge started for that shell skips automatic
|
|
@@ -203,10 +209,12 @@ localhost. The `AgentHost` extension owns one FIFO reader per agent session and
|
|
|
203
209
|
fans agent events out to every subscriber.
|
|
204
210
|
|
|
205
211
|
`peach open` auto-installs a published runtime only when the runtime directory
|
|
206
|
-
is empty
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
is empty and no live bridge already owns the runtime. When a live bridge is
|
|
213
|
+
performing its first download, `open` waits for that bridge's bootstrap state
|
|
214
|
+
to finish, then uses its installed runtime. Without a live bridge, runtime
|
|
215
|
+
files without a selected runtime containing `index.html` still cause a refusal.
|
|
216
|
+
Finish copying the runtime, then select it with `peach runtime use <version>`.
|
|
217
|
+
Write the active marker after staging is complete when preparing an isolated home.
|
|
210
218
|
|
|
211
219
|
During source development, each Vite bridge owns one
|
|
212
220
|
`~/.peach/dev-bridges/<port>.json` heartbeat record. CLI defaults select the
|
|
@@ -215,12 +223,13 @@ bridge, so concurrent worktrees cannot replace or delete one another's address.
|
|
|
215
223
|
Each Vite shell also publishes its exact bridge at `/__sootsim/bridge`, so a
|
|
216
224
|
launcher that reuses a shell from another isolated `PEACH_HOME` stays in that
|
|
217
225
|
shell's bridge world without reading the other process's lockfile directory.
|
|
218
|
-
An explicit `--port`
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
SDK `connect({ sim })
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
An explicit CLI `--port` or SDK port connects directly to that bridge without
|
|
227
|
+
scanning other worlds or requiring a local heartbeat record. That bridge handles
|
|
228
|
+
the named sim and reports an error if it is absent.
|
|
229
|
+
Without an explicit port, `--sim`, SDK `connect({ sim })`, and batched `do --then`
|
|
230
|
+
actions discover the sim's owning world. Each world probe has one two-second
|
|
231
|
+
wall deadline covering both connection and response. An inferred default port
|
|
232
|
+
does not override the named sim's world.
|
|
224
233
|
|
|
225
234
|
**The sim** is a browser/Electron *page* launched by a CLI driver. Inside that
|
|
226
235
|
page run three workers: the **shell worker** owns device and native state, the
|
|
@@ -443,6 +452,7 @@ peach debug enable animated,layout
|
|
|
443
452
|
peach debug trace shell on 240
|
|
444
453
|
peach do tap-text "..." # interact
|
|
445
454
|
peach do tap email --then type user@example.com --then tap submit
|
|
455
|
+
peach shell window --x 120 --y 140 --scale fit # place/scale desktop sim window
|
|
446
456
|
peach debug snapshot before
|
|
447
457
|
peach debug snapshot after
|
|
448
458
|
peach debug diff before after
|
|
@@ -551,6 +561,16 @@ peach detox # auto-discovers e2e/ tests and launches a shell
|
|
|
551
561
|
peach detox init # scaffold a config + sample test
|
|
552
562
|
```
|
|
553
563
|
|
|
564
|
+
`device.launchApp()` reuses a running guest by default. With a URL,
|
|
565
|
+
`device.launchApp({ url })` and `device.openURL({ url })` deliver a Linking
|
|
566
|
+
URL event to the running guest through the shell's deep-link bridge. Warm `launchApp` brings the guest forward; `openURL` retains its
|
|
567
|
+
foreground or background state. Both retain its browser session and app state.
|
|
568
|
+
`sourceApp` does not add fields to React Native's `{ url }` event. A
|
|
569
|
+
stopped guest rejects URL delivery. Explicit `newInstance: true`, deletion,
|
|
570
|
+
and video recording create a new browser context; cold launches still use the
|
|
571
|
+
supplied shell URL. When synchronization is enabled, URL delivery waits for
|
|
572
|
+
existing work before dispatch and for navigation and painting afterward.
|
|
573
|
+
|
|
554
574
|
See `../../docs/migrating-from-detox.md` for details.
|
|
555
575
|
|
|
556
576
|
## Coming from Maestro
|
|
@@ -604,6 +624,9 @@ the framed export inline without changing Maestro syntax:
|
|
|
604
624
|
native content, a populated node tree, or a live-frame channel whose publish
|
|
605
625
|
count advances across probes. Canvas-only GL, WebGPU, and video surfaces can
|
|
606
626
|
therefore prove that they are painting without weakening the boot-card guard.
|
|
627
|
+
A confirmed guest launch error ends readiness immediately, including waits longer
|
|
628
|
+
than one bridge request. Loading and suppressed entry errors keep their existing
|
|
629
|
+
readiness rules.
|
|
607
630
|
|
|
608
631
|
```yaml
|
|
609
632
|
- takeScreenshot: hero
|
package/cli/app-catalog.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import {
|
|
4
4
|
enumerateBundleColors,
|
|
5
5
|
enumerateBundleImages,
|
|
6
|
+
enumerateBundleSymbols,
|
|
6
7
|
} from '../../sootsim-swift/src/bundleAssets'
|
|
7
8
|
import type { AppCatalog } from 'peach-engine/engine/app-catalog-wire'
|
|
8
9
|
|
|
@@ -36,7 +37,7 @@ function catalogFiles(dir: string, found: string[]) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// the app's asset-catalog colors
|
|
40
|
+
// the app's asset-catalog colors, images, symbols and declared accent, read off
|
|
40
41
|
// the ios project a prebuild or a bare app keeps on disk: the app target is
|
|
41
42
|
// the folder holding the AppDelegate, the catalogs are its .xcassets, and the
|
|
42
43
|
// accent is what its Info.plist or project build settings name. metro serves
|
|
@@ -65,9 +66,9 @@ export function discoverAppCatalog(
|
|
|
65
66
|
const relative = files.map((file) =>
|
|
66
67
|
path.relative(targetDir, file).split(path.sep).join(path.posix.sep),
|
|
67
68
|
)
|
|
68
|
-
const readContents = (candidate: string) =>
|
|
69
|
-
candidate.endsWith('Contents.json') ? readText(path.join(targetDir, candidate)) : null
|
|
69
|
+
const readContents = (candidate: string) => readText(path.join(targetDir, candidate))
|
|
70
70
|
const colors = enumerateBundleColors(relative, readContents)
|
|
71
|
+
const symbols = enumerateBundleSymbols(relative, readContents, () => '.')
|
|
71
72
|
const served = path.relative(appDir, targetDir).split(path.sep).join(path.posix.sep)
|
|
72
73
|
const images: AppCatalog['images'] = {}
|
|
73
74
|
for (const { name, file, scale } of enumerateBundleImages(relative, readContents)) {
|
|
@@ -85,8 +86,9 @@ export function discoverAppCatalog(
|
|
|
85
86
|
if (
|
|
86
87
|
accentName === null &&
|
|
87
88
|
Object.keys(colors).length === 0 &&
|
|
88
|
-
Object.keys(images).length === 0
|
|
89
|
+
Object.keys(images).length === 0 &&
|
|
90
|
+
Object.keys(symbols).length === 0
|
|
89
91
|
)
|
|
90
92
|
return null
|
|
91
|
-
return { colors, images, accentName }
|
|
93
|
+
return { colors, images, symbols, accentName }
|
|
92
94
|
}
|
package/cli/app-config.ts
CHANGED
|
@@ -4,7 +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
|
+
import { resolveAppStubs, wireSlotMigration, type AppStubs } from './app-stubs'
|
|
8
8
|
|
|
9
9
|
// `{ file }` entries are relative to the config file; the engine loads them by
|
|
10
10
|
// absolute path from the runtime's replacement-module endpoint.
|
|
@@ -86,8 +86,12 @@ async function loadConfigFile(
|
|
|
86
86
|
}
|
|
87
87
|
const wire = WIRE_SLOTS.filter((slot) => loaded[slot] !== undefined)
|
|
88
88
|
if (wire.length > 0) {
|
|
89
|
+
const migration = wireSlotMigration(project.appDir, {
|
|
90
|
+
modules: { ...loaded.turboModules, ...loaded.nativeModules },
|
|
91
|
+
views: { ...loaded.nativeViews },
|
|
92
|
+
})
|
|
89
93
|
throw new Error(
|
|
90
|
-
`${wire.join(', ')} moved under packages, keyed by the package whose native side they answer
|
|
94
|
+
`${wire.join(', ')} moved under packages, keyed by the package whose native side they answer. replace them with:\n${migration}`,
|
|
91
95
|
)
|
|
92
96
|
}
|
|
93
97
|
return resolveConfigReplacementFiles(loaded, project.configPath)
|
package/cli/app-fonts.ts
CHANGED
|
@@ -7,14 +7,20 @@ import {
|
|
|
7
7
|
expoFontPathsFromPlugins,
|
|
8
8
|
nativeLinkedFontSpecsFromPaths,
|
|
9
9
|
metroFontSpecsFromPaths,
|
|
10
|
+
appFontAssetUrlsFromWire,
|
|
10
11
|
type ConfigPluginFontSpec,
|
|
11
12
|
type FontPlatform,
|
|
12
13
|
} from 'peach-engine/engine/config-plugin-fonts'
|
|
13
14
|
import { resolveListeningPid, resolveProcessCwd } from '../scripts/dev-server-scanner'
|
|
14
15
|
import { loopbackOriginOf } from '../src/backend-origin'
|
|
15
16
|
import { MANIFEST_PROBE_TIMEOUT_MS } from '../src/dev-bundle-resolution'
|
|
17
|
+
import { stageHostFile } from '../src/host/staged-host-files'
|
|
16
18
|
import { parsePlistSource } from '../src/plist.ts'
|
|
17
19
|
import { resolveAppProject } from './app-project'
|
|
20
|
+
import {
|
|
21
|
+
materialSymbolFontPaths,
|
|
22
|
+
materialSymbolFontAssets,
|
|
23
|
+
} from './material-symbol-fonts'
|
|
18
24
|
|
|
19
25
|
const require = createRequire(import.meta.url)
|
|
20
26
|
|
|
@@ -33,14 +39,20 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
33
39
|
bundleUrl: string
|
|
34
40
|
projectDir: string
|
|
35
41
|
platform?: string
|
|
42
|
+
nativeAssetOrigin: string
|
|
36
43
|
}): DiscoveredAppFontSpec[] {
|
|
37
44
|
const metroOrigin = loopbackOriginOf(opts.bundleUrl)
|
|
38
45
|
if (!metroOrigin) return []
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
const { paths, materialSymbols } = discoverNativeLinkedAppFontAssets(opts)
|
|
47
|
+
const specs = metroFontSpecsFromPaths(
|
|
48
|
+
projectRelativeFontPaths(opts.projectDir, paths),
|
|
41
49
|
metroOrigin,
|
|
42
50
|
nativeFontPlatform(opts.platform),
|
|
43
51
|
)
|
|
52
|
+
attachMaterialSymbolMetadata(specs, materialSymbols, (asset) =>
|
|
53
|
+
stageHostFile(asset, opts.nativeAssetOrigin),
|
|
54
|
+
)
|
|
55
|
+
return specs
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
function nativeFontPlatform(platform: string | undefined): FontPlatform {
|
|
@@ -49,10 +61,40 @@ function nativeFontPlatform(platform: string | undefined): FontPlatform {
|
|
|
49
61
|
: 'ios'
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
function attachMaterialSymbolMetadata(
|
|
65
|
+
specs: ConfigPluginFontSpec[],
|
|
66
|
+
files: ReturnType<typeof materialSymbolFontAssets>,
|
|
67
|
+
assetUrl: (filePath: string) => string,
|
|
68
|
+
): void {
|
|
69
|
+
const first = specs.find(
|
|
70
|
+
(spec) =>
|
|
71
|
+
spec.family !== undefined &&
|
|
72
|
+
/^MaterialSymbols(?:Outlined|Rounded|Sharp)_/.test(spec.family),
|
|
55
73
|
)
|
|
74
|
+
if (first && files)
|
|
75
|
+
first.materialSymbols = {
|
|
76
|
+
hash: files.hash,
|
|
77
|
+
codepointsUrl: assetUrl(files.codepointsPath),
|
|
78
|
+
mirroredUrl: assetUrl(files.mirroredPath),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function projectRelativeFontPaths(projectDir: string, fontPaths: string[]): string[] {
|
|
83
|
+
// metro resolves an asset path lexically against its project root and reads
|
|
84
|
+
// through symlinks, so a font declared inside the project keeps its declared
|
|
85
|
+
// path even when node_modules is a symlink to a shared install. a font that
|
|
86
|
+
// node resolved outside the project (a hoisted workspace or pnpm store) is
|
|
87
|
+
// named through its real location, in the namespace of the real project root.
|
|
88
|
+
const projectRoot = path.resolve(projectDir)
|
|
89
|
+
const realProjectRoot = fs.realpathSync(projectDir)
|
|
90
|
+
return fontPaths.map((fontPath) => {
|
|
91
|
+
const lexical = path.relative(projectRoot, fontPath)
|
|
92
|
+
const relative =
|
|
93
|
+
lexical.startsWith('..') || path.isAbsolute(lexical)
|
|
94
|
+
? path.relative(realProjectRoot, fs.realpathSync(fontPath))
|
|
95
|
+
: lexical
|
|
96
|
+
return relative.split(path.sep).join('/')
|
|
97
|
+
})
|
|
56
98
|
}
|
|
57
99
|
|
|
58
100
|
function isFile(filePath: string): boolean {
|
|
@@ -64,13 +106,13 @@ function isFile(filePath: string): boolean {
|
|
|
64
106
|
}
|
|
65
107
|
|
|
66
108
|
// absolute paths of every existing font file the native app links.
|
|
67
|
-
function
|
|
109
|
+
function discoverNativeLinkedAppFontAssets(opts: {
|
|
68
110
|
projectDir: string
|
|
69
111
|
platform?: string
|
|
70
|
-
}): string[] {
|
|
112
|
+
}): { paths: string[]; materialSymbols?: ReturnType<typeof materialSymbolFontAssets> } {
|
|
71
113
|
const project = resolveAppProject(opts.projectDir)
|
|
72
114
|
const selectedAppDir = project?.appDir ?? null
|
|
73
|
-
if (!selectedAppDir) return []
|
|
115
|
+
if (!selectedAppDir) return { paths: [] }
|
|
74
116
|
|
|
75
117
|
const platform = nativeFontPlatform(opts.platform)
|
|
76
118
|
|
|
@@ -105,6 +147,9 @@ function discoverNativeLinkedAppFontPaths(opts: {
|
|
|
105
147
|
.slice(0, 256)
|
|
106
148
|
.map((fontPath) => path.resolve(selectedAppDir, fontPath))
|
|
107
149
|
.filter(isFile)
|
|
150
|
+
const materialFonts =
|
|
151
|
+
platform === 'android' ? materialSymbolFontPaths(selectedAppDir) : []
|
|
152
|
+
fontPaths.push(...materialFonts)
|
|
108
153
|
// the walk is bounded by WORK, not by a clock: at most 64 readdirs, at most 64
|
|
109
154
|
// queued dirs, 4 levels deep, 256 paths. those ceilings are what keep a stray
|
|
110
155
|
// node_modules from being crawled, and no machine can blow through them. a
|
|
@@ -271,7 +316,10 @@ function discoverNativeLinkedAppFontPaths(opts: {
|
|
|
271
316
|
} catch {}
|
|
272
317
|
}
|
|
273
318
|
|
|
274
|
-
return
|
|
319
|
+
return {
|
|
320
|
+
paths: [...new Set(fontPaths)],
|
|
321
|
+
materialSymbols: materialSymbolFontAssets(materialFonts),
|
|
322
|
+
}
|
|
275
323
|
}
|
|
276
324
|
|
|
277
325
|
// prebuilt guests cannot discover files linked into their native binary.
|
|
@@ -283,16 +331,23 @@ export function readNativeLinkedAppFontsWire(opts: {
|
|
|
283
331
|
if (!resolveAppProject(opts.projectDir)) {
|
|
284
332
|
throw new Error(`native font project is unavailable: ${opts.projectDir}`)
|
|
285
333
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
334
|
+
const projectDir = fs.realpathSync(opts.projectDir)
|
|
335
|
+
const { paths, materialSymbols } = discoverNativeLinkedAppFontAssets(opts)
|
|
336
|
+
const specs = nativeLinkedFontSpecsFromPaths(
|
|
337
|
+
projectRelativeFontPaths(projectDir, paths),
|
|
338
|
+
nativeFontPlatform(opts.platform),
|
|
339
|
+
(fontPath) => {
|
|
340
|
+
const bytes = fs.readFileSync(path.resolve(projectDir, fontPath))
|
|
341
|
+
return `data:${fontContentType(fontPath)};base64,${bytes.toString('base64')}`
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
attachMaterialSymbolMetadata(
|
|
345
|
+
specs,
|
|
346
|
+
materialSymbols,
|
|
347
|
+
(filePath) =>
|
|
348
|
+
`data:application/octet-stream;base64,${fs.readFileSync(filePath).toString('base64')}`,
|
|
295
349
|
)
|
|
350
|
+
return encodeAppFontsWire(specs)
|
|
296
351
|
}
|
|
297
352
|
|
|
298
353
|
// resolve a metro dev server's project root. Expo's middleware advertises it
|
|
@@ -357,22 +412,6 @@ export async function resolveMetroProjectRoot(bundleUrl: string): Promise<string
|
|
|
357
412
|
return pid === null ? null : resolveProcessCwd(pid)
|
|
358
413
|
}
|
|
359
414
|
|
|
360
|
-
export function parseAppFontUrls(wire: string): string[] {
|
|
361
|
-
const urls: string[] = []
|
|
362
|
-
for (const entry of wire.split(',')) {
|
|
363
|
-
const trimmed = entry.trim()
|
|
364
|
-
if (!trimmed) continue
|
|
365
|
-
const sep = trimmed.indexOf('::')
|
|
366
|
-
const urlPart = sep === -1 ? trimmed : trimmed.slice(0, sep)
|
|
367
|
-
try {
|
|
368
|
-
urls.push(decodeURIComponent(urlPart))
|
|
369
|
-
} catch {
|
|
370
|
-
urls.push(urlPart)
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return urls.filter(Boolean)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
415
|
// content type for a config-plugin font url, by extension. config fonts only
|
|
377
416
|
// ever ship as ttf/otf/woff(2); fall back to octet-stream for anything else
|
|
378
417
|
// so R2 still serves bytes the engine can register.
|
|
@@ -409,7 +448,7 @@ export async function fetchAppFontFiles(
|
|
|
409
448
|
): Promise<FetchedAppFont[]> {
|
|
410
449
|
const out: FetchedAppFont[] = []
|
|
411
450
|
const seen = new Set<string>()
|
|
412
|
-
for (const url of
|
|
451
|
+
for (const url of appFontAssetUrlsFromWire(wire)) {
|
|
413
452
|
const urlhash = createHash('sha256').update(url).digest('hex')
|
|
414
453
|
if (seen.has(urlhash)) continue
|
|
415
454
|
seen.add(urlhash)
|
package/cli/app-stubs.ts
CHANGED
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
7
7
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
8
8
|
import { resolveCompatVersionRange } from '@peach/compat/version-range'
|
|
9
|
+
import { parseSync, type Expression } from 'oxc-parser'
|
|
10
|
+
import { transform } from 'sucrase'
|
|
9
11
|
import {
|
|
10
12
|
isPackageStubs,
|
|
11
13
|
type NativeModuleResolution,
|
|
12
14
|
type NativeStub,
|
|
13
15
|
type NativeViewResolution,
|
|
16
|
+
type NativeViewSource,
|
|
14
17
|
type PackageStubs,
|
|
15
18
|
type PeachConfig,
|
|
16
19
|
} from '../src/config'
|
|
17
20
|
import { isRecord } from '../src/is-record'
|
|
21
|
+
import { visitParsedProgram } from '../src/visit-parsed-program'
|
|
18
22
|
import { readProjectManifest, scanRepository } from './commands/detect'
|
|
19
23
|
|
|
20
24
|
// who answers a package, in rising precedence
|
|
@@ -28,6 +32,7 @@ export interface AppliedStubs {
|
|
|
28
32
|
providerVersion: string | null
|
|
29
33
|
modules: string[]
|
|
30
34
|
views: string[]
|
|
35
|
+
viewSources?: Record<string, NativeViewSource>
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
// a published stub whose `versions` range excludes the installed target
|
|
@@ -141,7 +146,8 @@ export function resolveAppStubs(
|
|
|
141
146
|
for (const [name, stub] of Object.entries(modules))
|
|
142
147
|
nativeModules[name] = lowerStub(stub, source.baseDir)
|
|
143
148
|
for (const [name, stub] of Object.entries(views))
|
|
144
|
-
nativeViews[name] =
|
|
149
|
+
nativeViews[name] =
|
|
150
|
+
typeof stub === 'string' ? lowerStub(stub, source.baseDir) : stub
|
|
145
151
|
applied.push({
|
|
146
152
|
target: source.target,
|
|
147
153
|
origin: source.origin,
|
|
@@ -149,6 +155,16 @@ export function resolveAppStubs(
|
|
|
149
155
|
providerVersion: source.providerVersion,
|
|
150
156
|
modules: Object.keys(modules),
|
|
151
157
|
views: Object.keys(views),
|
|
158
|
+
...(Object.values(views).some((value) => typeof value !== 'string')
|
|
159
|
+
? {
|
|
160
|
+
viewSources: Object.fromEntries(
|
|
161
|
+
Object.entries(views).filter(
|
|
162
|
+
(entry): entry is [string, NativeViewSource] =>
|
|
163
|
+
typeof entry[1] !== 'string',
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
152
168
|
})
|
|
153
169
|
}
|
|
154
170
|
const stubs = { applied, skipped }
|
|
@@ -162,7 +178,10 @@ export function resolveAppStubs(
|
|
|
162
178
|
function expandStubs(
|
|
163
179
|
appDir: string,
|
|
164
180
|
source: StubSource,
|
|
165
|
-
): {
|
|
181
|
+
): {
|
|
182
|
+
modules: Record<string, NativeStub>
|
|
183
|
+
views: Record<string, NativeStub | NativeViewSource>
|
|
184
|
+
} {
|
|
166
185
|
if (source.stubs !== 'noop')
|
|
167
186
|
return { modules: source.stubs.modules ?? {}, views: source.stubs.views ?? {} }
|
|
168
187
|
const dir = installedPackageDir(appDir, source.target)
|
|
@@ -212,34 +231,85 @@ export interface NativeRequest {
|
|
|
212
231
|
}
|
|
213
232
|
|
|
214
233
|
export const SOURCE_FILE = /\.[cm]?[jt]sx?$/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
234
|
+
const NATIVE_LOOKUP =
|
|
235
|
+
/requireNative|requireOptionalNative|TurboModuleRegistry|codegenNativeComponent|NativeModules/
|
|
236
|
+
|
|
237
|
+
function nativeLookupName(node: Expression): string | null {
|
|
238
|
+
if (node.type === 'Identifier') return node.name
|
|
239
|
+
if (
|
|
240
|
+
node.type === 'MemberExpression' &&
|
|
241
|
+
!node.computed &&
|
|
242
|
+
node.property.type === 'Identifier'
|
|
243
|
+
)
|
|
244
|
+
return node.property.name
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// sucrase removes flow and typescript types while retaining source lines. oxc
|
|
249
|
+
// then visits executable syntax, including expressions inside template literals,
|
|
250
|
+
// without interpreting comments, fixture strings or type queries as requests.
|
|
251
|
+
export function sourceNativeRequests(
|
|
252
|
+
file: string,
|
|
253
|
+
text: string,
|
|
254
|
+
includeLegacy = false,
|
|
255
|
+
): (NativeRequest & { line: number })[] {
|
|
256
|
+
if (!NATIVE_LOOKUP.test(text)) return []
|
|
257
|
+
let code: string
|
|
258
|
+
try {
|
|
259
|
+
code = transform(text, {
|
|
260
|
+
transforms: [/\.[cm]?tsx?$/.test(file) ? 'typescript' : 'flow', 'jsx'],
|
|
261
|
+
disableESTransforms: true,
|
|
262
|
+
}).code
|
|
263
|
+
} catch {
|
|
264
|
+
return []
|
|
265
|
+
}
|
|
266
|
+
const { program, errors } = parseSync(file, code, { lang: 'jsx' })
|
|
267
|
+
if (errors.length) return []
|
|
268
|
+
const found: (NativeRequest & { line: number })[] = []
|
|
269
|
+
const add = (kind: NativeRequest['kind'], name: string, at: number) => {
|
|
270
|
+
found.push({ kind, name, line: code.slice(0, at).split('\n').length })
|
|
271
|
+
}
|
|
272
|
+
visitParsedProgram(program, {
|
|
273
|
+
CallExpression(node) {
|
|
274
|
+
const first = node.arguments[0]
|
|
275
|
+
if (first?.type !== 'Literal' || typeof first.value !== 'string') return
|
|
276
|
+
const callee = node.callee
|
|
277
|
+
const lookup = nativeLookupName(callee)
|
|
278
|
+
if (
|
|
279
|
+
callee.type === 'MemberExpression' &&
|
|
280
|
+
!callee.computed &&
|
|
281
|
+
nativeLookupName(callee.object) === 'TurboModuleRegistry' &&
|
|
282
|
+
(lookup === 'get' || lookup === 'getEnforcing')
|
|
283
|
+
) {
|
|
284
|
+
add('module', first.value, node.start)
|
|
285
|
+
} else if (lookup) {
|
|
286
|
+
if (['requireNativeModule', 'requireOptionalNativeModule'].includes(lookup))
|
|
287
|
+
add('module', first.value, node.start)
|
|
288
|
+
else if (['requireNativeComponent', 'codegenNativeComponent'].includes(lookup))
|
|
289
|
+
add('view', first.value, node.start)
|
|
290
|
+
else if (['requireNativeView', 'requireNativeViewManager'].includes(lookup)) {
|
|
291
|
+
const second = node.arguments[1]
|
|
292
|
+
// expo registers a named view under the module_view key.
|
|
293
|
+
const name =
|
|
294
|
+
second?.type === 'Literal' && typeof second.value === 'string'
|
|
295
|
+
? `${first.value}_${second.value}`
|
|
296
|
+
: first.value
|
|
297
|
+
add('view', name, node.start)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
MemberExpression(node) {
|
|
302
|
+
if (
|
|
303
|
+
includeLegacy &&
|
|
304
|
+
!node.computed &&
|
|
305
|
+
nativeLookupName(node.object) === 'NativeModules' &&
|
|
306
|
+
node.property.type === 'Identifier'
|
|
307
|
+
)
|
|
308
|
+
add('module', node.property.name, node.start)
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
return found
|
|
312
|
+
}
|
|
243
313
|
|
|
244
314
|
// the native names a package's own JavaScript asks for, outside its native
|
|
245
315
|
// sources (top-level ios and android; src/ios is JavaScript), tests and examples
|
|
@@ -251,7 +321,6 @@ const PACKAGE_SKIP_DIRS = new Set([
|
|
|
251
321
|
'__mocks__',
|
|
252
322
|
'docs',
|
|
253
323
|
])
|
|
254
|
-
const LEGACY_NATIVE_MODULE = /\bNativeModules\s*\.\s*(\w+)/g
|
|
255
324
|
|
|
256
325
|
export function packageNativeRequests(dir: string): NativeRequest[] {
|
|
257
326
|
const found = new Map<string, NativeRequest>()
|
|
@@ -267,17 +336,11 @@ export function packageNativeRequests(dir: string): NativeRequest[] {
|
|
|
267
336
|
for (const name of listDir(sub)) {
|
|
268
337
|
if (!SOURCE_FILE.test(name) || name.endsWith('.d.ts') || files >= 2_000) continue
|
|
269
338
|
files++
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
{
|
|
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
|
-
}
|
|
339
|
+
const file = join(sub, name)
|
|
340
|
+
const text = readSmallFile(file)
|
|
341
|
+
if (!text) continue
|
|
342
|
+
for (const { kind, name } of sourceNativeRequests(file, text, true))
|
|
343
|
+
found.set(`${kind}:${name}`, { kind, name })
|
|
281
344
|
}
|
|
282
345
|
}
|
|
283
346
|
return [...found.values()].sort(
|
|
@@ -285,6 +348,74 @@ export function packageNativeRequests(dir: string): NativeRequest[] {
|
|
|
285
348
|
)
|
|
286
349
|
}
|
|
287
350
|
|
|
351
|
+
// the `packages` block that replaces a config's old top-level wire slots: each
|
|
352
|
+
// name keyed by the direct dependency whose JavaScript asks for it. `packages`
|
|
353
|
+
// takes only 'noop' or a file path, so any other old value is named for a hand
|
|
354
|
+
// move instead of guessed.
|
|
355
|
+
export function wireSlotMigration(
|
|
356
|
+
appDir: string,
|
|
357
|
+
wire: { modules: Record<string, unknown>; views: Record<string, unknown> },
|
|
358
|
+
): string {
|
|
359
|
+
const owners = new Map<string, string>()
|
|
360
|
+
const manifest = readProjectManifest(appDir)
|
|
361
|
+
for (const name of Object.keys({
|
|
362
|
+
...manifest?.devDependencies,
|
|
363
|
+
...manifest?.dependencies,
|
|
364
|
+
})) {
|
|
365
|
+
const dir = installedPackageDir(appDir, name)
|
|
366
|
+
if (!dir) continue
|
|
367
|
+
for (const request of packageNativeRequests(dir))
|
|
368
|
+
if (!owners.has(`${request.kind}:${request.name}`))
|
|
369
|
+
owners.set(`${request.kind}:${request.name}`, name)
|
|
370
|
+
}
|
|
371
|
+
// the app's own native code answers under its package name
|
|
372
|
+
if (typeof manifest?.name === 'string')
|
|
373
|
+
for (const request of packageNativeRequests(appDir))
|
|
374
|
+
if (!owners.has(`${request.kind}:${request.name}`))
|
|
375
|
+
owners.set(`${request.kind}:${request.name}`, manifest.name)
|
|
376
|
+
const unknownOwner = '<package that asks for it>'
|
|
377
|
+
const blocks = new Map<string, { modules: string[]; views: string[] }>()
|
|
378
|
+
const manual: string[] = []
|
|
379
|
+
for (const [kind, entries] of [
|
|
380
|
+
['module', wire.modules],
|
|
381
|
+
['view', wire.views],
|
|
382
|
+
] as const) {
|
|
383
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
384
|
+
const stub =
|
|
385
|
+
value === 'noop'
|
|
386
|
+
? JSON.stringify(value)
|
|
387
|
+
: isRecord(value) &&
|
|
388
|
+
typeof value.file === 'string' &&
|
|
389
|
+
Object.keys(value).length === 1
|
|
390
|
+
? JSON.stringify(value.file)
|
|
391
|
+
: null
|
|
392
|
+
if (!stub) {
|
|
393
|
+
manual.push(
|
|
394
|
+
`${name}: packages takes 'noop' or a file path; move this ${kind} into a file that default-exports it and name that file`,
|
|
395
|
+
)
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
const owner = owners.get(`${kind}:${name}`) ?? unknownOwner
|
|
399
|
+
const block = blocks.get(owner) ?? { modules: [], views: [] }
|
|
400
|
+
block[kind === 'module' ? 'modules' : 'views'].push(
|
|
401
|
+
`[${JSON.stringify(name)}]: ${stub}`,
|
|
402
|
+
)
|
|
403
|
+
blocks.set(owner, block)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const lines = [...blocks].map(([owner, { modules, views }]) => {
|
|
407
|
+
const parts = [
|
|
408
|
+
modules.length ? `modules: { ${modules.join(', ')} }` : '',
|
|
409
|
+
views.length ? `views: { ${views.join(', ')} }` : '',
|
|
410
|
+
].filter(Boolean)
|
|
411
|
+
return ` [${JSON.stringify(owner)}]: { ${parts.join(', ')} },`
|
|
412
|
+
})
|
|
413
|
+
return [
|
|
414
|
+
...(lines.length ? [' packages: {', ...lines, ' },'] : []),
|
|
415
|
+
...manual.map((line) => ` // ${line}`),
|
|
416
|
+
].join('\n')
|
|
417
|
+
}
|
|
418
|
+
|
|
288
419
|
export function listDir(dir: string): string[] {
|
|
289
420
|
try {
|
|
290
421
|
return readdirSync(dir)
|
package/cli/bin.ts
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
import { exitWithFlushedOutput } from '../src/process-output'
|
|
14
14
|
import { RNX_FORKED_SCRIPT_ENV, RNX_INTERNAL_COMMAND } from './internal-child'
|
|
15
15
|
|
|
16
|
+
// a consumer such as head may close its pipe before the command finishes.
|
|
17
|
+
// exit directly: reporting this through stderr can fail on the same pipe.
|
|
18
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
19
|
+
stream.on('error', (error: NodeJS.ErrnoException) => {
|
|
20
|
+
if (error.code === 'EPIPE') process.exit(1)
|
|
21
|
+
throw error
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
if (process.argv[2] === '__shell-init') {
|
|
17
26
|
const { renderRnxShellInit } = await import('./shell-init')
|
|
18
27
|
try {
|