peachsim 0.1.527
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 +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,4268 @@
|
|
|
1
|
+
/*! rnx v0.1.527 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
|
+
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name2 in all)
|
|
12
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
23
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
24
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
25
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
26
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
27
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
28
|
+
mod
|
|
29
|
+
));
|
|
30
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
31
|
+
|
|
32
|
+
// src/index.ts
|
|
33
|
+
var src_exports = {};
|
|
34
|
+
__export(src_exports, {
|
|
35
|
+
DEFAULT_SOOTSIM_BRIDGE_PORT: () => DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
36
|
+
DEFAULT_SOOTSIM_BRIDGE_URL: () => DEFAULT_SOOTSIM_BRIDGE_URL,
|
|
37
|
+
DEFAULT_SOOTSIM_SHELL_URL: () => DEFAULT_SOOTSIM_SHELL_URL,
|
|
38
|
+
RNX_SCREEN_CAPTURE_VERSION: () => RNX_SCREEN_CAPTURE_VERSION,
|
|
39
|
+
SimClient: () => SimClient,
|
|
40
|
+
SimClientError: () => SimClientError,
|
|
41
|
+
SimConnectError: () => SimConnectError,
|
|
42
|
+
SimKeyboardError: () => SimKeyboardError,
|
|
43
|
+
SimPerformError: () => SimPerformError,
|
|
44
|
+
SimReadyTimeoutError: () => SimReadyTimeoutError,
|
|
45
|
+
SimResetError: () => SimResetError,
|
|
46
|
+
SimTapError: () => SimTapError,
|
|
47
|
+
connect: () => connect,
|
|
48
|
+
isRnxCaptureRegionAtlas: () => isRnxCaptureRegionAtlas,
|
|
49
|
+
isRnxScreenCapture: () => isRnxScreenCapture,
|
|
50
|
+
jumpToSourceBabelPlugin: () => sootsimJumpToSourceBabelPlugin,
|
|
51
|
+
metroPlugin: () => metro_plugin_default,
|
|
52
|
+
rnxCaptureRasterRegions: () => rnxCaptureRasterRegions,
|
|
53
|
+
rnxPlugin: () => rnxPlugin,
|
|
54
|
+
rnxRasterFrames: () => rnxRasterFrames,
|
|
55
|
+
selectRnxRasterNodeIds: () => selectRnxRasterNodeIds
|
|
56
|
+
});
|
|
57
|
+
module.exports = __toCommonJS(src_exports);
|
|
58
|
+
|
|
59
|
+
// cli/cloud-client.ts
|
|
60
|
+
var import_node_crypto = require("node:crypto");
|
|
61
|
+
|
|
62
|
+
// ../sootsim-engine/src/metro-fingerprint-registry-client.ts
|
|
63
|
+
var import_metro_fingerprint_registry = require("rnxsim/metro-fingerprint-registry");
|
|
64
|
+
|
|
65
|
+
// ../sootsim-engine/src/metro-fingerprint.ts
|
|
66
|
+
var import_metro_production_bundle = require("rnxsim/metro-production-bundle");
|
|
67
|
+
var import_react_native_host_modules = require("rnxsim/react-native-host-modules");
|
|
68
|
+
|
|
69
|
+
// src/backend-origin.ts
|
|
70
|
+
function normalizeHostname(hostname) {
|
|
71
|
+
return hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
function isLoopbackHost(hostname) {
|
|
74
|
+
const normalized = normalizeHostname(hostname);
|
|
75
|
+
return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "0.0.0.0" || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/cloud-contract.ts
|
|
79
|
+
var RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024;
|
|
80
|
+
var RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024;
|
|
81
|
+
var RNX_CLOUD_COMMAND_SCOPES = {
|
|
82
|
+
state: "read",
|
|
83
|
+
tree: "read",
|
|
84
|
+
query: "read",
|
|
85
|
+
resolve: "read",
|
|
86
|
+
screenshot: "read",
|
|
87
|
+
capture: "read",
|
|
88
|
+
captureRegions: "read",
|
|
89
|
+
memory: "read",
|
|
90
|
+
diagnostics: "read",
|
|
91
|
+
storageSnapshot: "read",
|
|
92
|
+
perform: "drive",
|
|
93
|
+
waitFor: "drive",
|
|
94
|
+
tap: "drive",
|
|
95
|
+
longPress: "drive",
|
|
96
|
+
settle: "drive",
|
|
97
|
+
openUrl: "drive",
|
|
98
|
+
setAppearance: "drive",
|
|
99
|
+
keyboard: "drive",
|
|
100
|
+
focus: "drive",
|
|
101
|
+
flowStatus: "drive",
|
|
102
|
+
reset: "reset",
|
|
103
|
+
camera: "media",
|
|
104
|
+
// string evaluation and arbitrary bridge calls are maestro's boundary: the
|
|
105
|
+
// customer sdk never carries this scope, and the service grants it only to
|
|
106
|
+
// the account api key caller. close rides along, since ending the page ends
|
|
107
|
+
// the sim and belongs to the sim's owner rather than any driver.
|
|
108
|
+
evaluate: "debug",
|
|
109
|
+
call: "debug",
|
|
110
|
+
close: "debug"
|
|
111
|
+
};
|
|
112
|
+
function scopeTableKeys() {
|
|
113
|
+
return Object.keys(RNX_CLOUD_COMMAND_SCOPES).filter(
|
|
114
|
+
(key) => Object.prototype.hasOwnProperty.call(RNX_CLOUD_COMMAND_SCOPES, key)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
var RNX_CLOUD_COMMAND_TYPES = Object.freeze(scopeTableKeys());
|
|
118
|
+
var RNX_CLOUD_COMMAND_TYPE_SET = new Set(RNX_CLOUD_COMMAND_TYPES);
|
|
119
|
+
function isRnxCloudCommandType(value) {
|
|
120
|
+
return RNX_CLOUD_COMMAND_TYPE_SET.has(value);
|
|
121
|
+
}
|
|
122
|
+
function rnxCloudCommandScope(type) {
|
|
123
|
+
return RNX_CLOUD_COMMAND_SCOPES[type];
|
|
124
|
+
}
|
|
125
|
+
var RNX_CLOUD_TOKEN_SCOPES = Object.freeze([
|
|
126
|
+
"read",
|
|
127
|
+
"drive",
|
|
128
|
+
"reset",
|
|
129
|
+
"media"
|
|
130
|
+
]);
|
|
131
|
+
var RNX_CLOUD_ALL_COMMAND_SCOPES = Object.freeze(["read", "drive", "reset", "media", "debug"]);
|
|
132
|
+
function rnxCloudScopesAllowCommand(scopes, type) {
|
|
133
|
+
return scopes.includes(RNX_CLOUD_COMMAND_SCOPES[type]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/metro-production-bundle.ts
|
|
137
|
+
var RNXSIM_METRO_MODULE_PATHS_PREFIX = "globalThis.__sootsimModulePaths=";
|
|
138
|
+
var RNX_METRO_MODULE_IDENTITY_VERSION = 2;
|
|
139
|
+
var RNXSIM_METRO_SOURCE_MAP_COMMENT = "//# sourceMappingURL=";
|
|
140
|
+
function detachRNXMetroSourceMapUrl(bundleText) {
|
|
141
|
+
const trimmedEnd = bundleText.replace(/\s+$/, "");
|
|
142
|
+
const commentStart = trimmedEnd.lastIndexOf(`
|
|
143
|
+
${RNXSIM_METRO_SOURCE_MAP_COMMENT}`);
|
|
144
|
+
if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null };
|
|
145
|
+
const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length);
|
|
146
|
+
if (url.includes("\n")) return { source: bundleText, sourceMappingUrl: null };
|
|
147
|
+
return { source: trimmedEnd.slice(0, commentStart), sourceMappingUrl: url };
|
|
148
|
+
}
|
|
149
|
+
function createRNXMetroModuleIdentityFooter(identity) {
|
|
150
|
+
const payload = JSON.stringify(identity).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/<\//g, "<\\/");
|
|
151
|
+
return `
|
|
152
|
+
${RNXSIM_METRO_MODULE_PATHS_PREFIX}${payload};`;
|
|
153
|
+
}
|
|
154
|
+
function appendRNXMetroModuleIdentityFooter(bundleText, identity) {
|
|
155
|
+
const footer = createRNXMetroModuleIdentityFooter(identity);
|
|
156
|
+
const detached = detachRNXMetroSourceMapUrl(bundleText);
|
|
157
|
+
return detached.sourceMappingUrl === null ? bundleText + footer : `${detached.source}${footer}
|
|
158
|
+
${RNXSIM_METRO_SOURCE_MAP_COMMENT}${detached.sourceMappingUrl}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/public-brand.ts
|
|
162
|
+
var name = "rnx";
|
|
163
|
+
var rnxPublicBrand = Object.freeze({
|
|
164
|
+
name,
|
|
165
|
+
packageName: "rnxsim",
|
|
166
|
+
commandName: "rnx",
|
|
167
|
+
domain: "rnxsim.com",
|
|
168
|
+
origin: "https://rnxsim.com"
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// src/home-paths.ts
|
|
172
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
173
|
+
var import_node_os = require("node:os");
|
|
174
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
175
|
+
var RNX_HOME_ENV = "RNX_HOME";
|
|
176
|
+
var ACTIVE_RUNTIME_FILE = "active";
|
|
177
|
+
var DAEMON_LOCKFILE = "daemon.json";
|
|
178
|
+
var DEV_BRIDGES_DIR = "dev-bridges";
|
|
179
|
+
var DAEMON_HEARTBEAT_STALE_MS = 3e4;
|
|
180
|
+
function rnxHomeDir() {
|
|
181
|
+
const override = process.env[RNX_HOME_ENV];
|
|
182
|
+
if (override && override.length > 0) return import_node_path.default.resolve(override);
|
|
183
|
+
return import_node_path.default.join((0, import_node_os.homedir)(), ".rnx");
|
|
184
|
+
}
|
|
185
|
+
function runtimesDir() {
|
|
186
|
+
return import_node_path.default.join(rnxHomeDir(), "runtimes");
|
|
187
|
+
}
|
|
188
|
+
function runtimeDir(version) {
|
|
189
|
+
return import_node_path.default.join(runtimesDir(), version);
|
|
190
|
+
}
|
|
191
|
+
function activeRuntimeFile() {
|
|
192
|
+
return import_node_path.default.join(runtimesDir(), ACTIVE_RUNTIME_FILE);
|
|
193
|
+
}
|
|
194
|
+
function daemonLockfilePath() {
|
|
195
|
+
return import_node_path.default.join(rnxHomeDir(), DAEMON_LOCKFILE);
|
|
196
|
+
}
|
|
197
|
+
function devBridgesDir() {
|
|
198
|
+
return import_node_path.default.join(rnxHomeDir(), DEV_BRIDGES_DIR);
|
|
199
|
+
}
|
|
200
|
+
function readActiveRuntime() {
|
|
201
|
+
try {
|
|
202
|
+
const value = import_node_fs.default.readFileSync(activeRuntimeFile(), "utf8").trim();
|
|
203
|
+
return value.length > 0 ? value : null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
|
|
209
|
+
function readDaemonLockfile() {
|
|
210
|
+
try {
|
|
211
|
+
const fd = import_node_fs.default.openSync(daemonLockfilePath(), "r");
|
|
212
|
+
try {
|
|
213
|
+
const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES);
|
|
214
|
+
const bytesRead = import_node_fs.default.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0);
|
|
215
|
+
const raw = buf.subarray(0, bytesRead).toString("utf8");
|
|
216
|
+
const parsed = JSON.parse(raw);
|
|
217
|
+
if (parsed && parsed.schema === 1 && typeof parsed.pid === "number" && typeof parsed.bridgePort === "number" && typeof parsed.runtimePort === "number" && typeof parsed.startedAt === "number" && typeof parsed.heartbeatAt === "number" && (parsed.servedRuntimes === void 0 || Array.isArray(parsed.servedRuntimes) && parsed.servedRuntimes.every((version) => typeof version === "string"))) {
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
} finally {
|
|
222
|
+
import_node_fs.default.closeSync(fd);
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function parseDevBridgeLockfile(raw) {
|
|
229
|
+
try {
|
|
230
|
+
const parsed = JSON.parse(raw);
|
|
231
|
+
if (parsed && parsed.schema === 1 && parsed.source === "vite-dev" && typeof parsed.pid === "number" && typeof parsed.bridgePort === "number" && typeof parsed.runtimePort === "number" && typeof parsed.cwd === "string" && typeof parsed.startedAt === "number" && typeof parsed.heartbeatAt === "number" && (parsed.shellPort === void 0 || typeof parsed.shellPort === "number") && (parsed.servedRuntimes === void 0 || Array.isArray(parsed.servedRuntimes) && parsed.servedRuntimes.every((version) => typeof version === "string"))) {
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function readDevBridgeLockfiles() {
|
|
240
|
+
let entries;
|
|
241
|
+
try {
|
|
242
|
+
entries = import_node_fs.default.readdirSync(devBridgesDir(), { withFileTypes: true });
|
|
243
|
+
} catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
const lockfiles = [];
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
if (!entry.isFile() || !/^\d+\.json$/.test(entry.name)) continue;
|
|
249
|
+
try {
|
|
250
|
+
const filePath = import_node_path.default.join(devBridgesDir(), entry.name);
|
|
251
|
+
const fd = import_node_fs.default.openSync(filePath, "r");
|
|
252
|
+
try {
|
|
253
|
+
const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES);
|
|
254
|
+
const bytesRead = import_node_fs.default.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0);
|
|
255
|
+
const lockfile = parseDevBridgeLockfile(
|
|
256
|
+
buf.subarray(0, bytesRead).toString("utf8")
|
|
257
|
+
);
|
|
258
|
+
if (lockfile && entry.name === `${lockfile.bridgePort}.json`) {
|
|
259
|
+
lockfiles.push(lockfile);
|
|
260
|
+
}
|
|
261
|
+
} finally {
|
|
262
|
+
import_node_fs.default.closeSync(fd);
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return lockfiles.sort((left, right) => left.bridgePort - right.bridgePort);
|
|
268
|
+
}
|
|
269
|
+
function isDaemonLockfileFresh(lock, now = Date.now()) {
|
|
270
|
+
if (!lock) return false;
|
|
271
|
+
if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
|
|
272
|
+
try {
|
|
273
|
+
process.kill(lock.pid, 0);
|
|
274
|
+
return true;
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function isDevBridgeLockfileFresh(lock, now = Date.now()) {
|
|
280
|
+
if (!lock) return false;
|
|
281
|
+
if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
|
|
282
|
+
try {
|
|
283
|
+
process.kill(lock.pid, 0);
|
|
284
|
+
return true;
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// cli/run-rnx.ts
|
|
291
|
+
var import_node_async_hooks = require("node:async_hooks");
|
|
292
|
+
var sinks = new import_node_async_hooks.AsyncLocalStorage();
|
|
293
|
+
|
|
294
|
+
// cli/auth.ts
|
|
295
|
+
var CHOOSE_ACCOUNT_HINT = "this sign-in belongs to several accounts; run `rnx login --account <name>` to choose the one to bill";
|
|
296
|
+
|
|
297
|
+
// cli/cloud-session.ts
|
|
298
|
+
var CLOUD_SESSION_VERSION = 3;
|
|
299
|
+
var MAX_ENCODED_SESSION_BYTES = 32 * 1024;
|
|
300
|
+
var RNX_CLOUD_SESSION_ENV = "RNX_CLOUD_SESSION";
|
|
301
|
+
var RNX_SHELL_UPDATE_PREFIX = "__RNX_SHELL_UPDATE_V1__";
|
|
302
|
+
var RNX_SHELL_UPDATE_SET_PREFIX = `${RNX_SHELL_UPDATE_PREFIX}set:`;
|
|
303
|
+
var RNX_SHELL_UPDATE_UNSET = `${RNX_SHELL_UPDATE_PREFIX}unset`;
|
|
304
|
+
function isRecord(value) {
|
|
305
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
306
|
+
}
|
|
307
|
+
function normalizeCloudOrigin(source) {
|
|
308
|
+
let parsed;
|
|
309
|
+
try {
|
|
310
|
+
parsed = new URL(source);
|
|
311
|
+
} catch {
|
|
312
|
+
throw new Error(`invalid remote simulator origin: ${source}`);
|
|
313
|
+
}
|
|
314
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
315
|
+
throw new Error(`remote simulator origin must be an HTTP(S) origin: ${source}`);
|
|
316
|
+
}
|
|
317
|
+
if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
|
|
318
|
+
throw new Error(`remote simulator origin requires HTTPS outside loopback: ${source}`);
|
|
319
|
+
}
|
|
320
|
+
return parsed.origin;
|
|
321
|
+
}
|
|
322
|
+
function createCloudSession(input) {
|
|
323
|
+
const boxId = input.boxId?.trim() ?? "";
|
|
324
|
+
const boxToken = input.boxToken ?? "";
|
|
325
|
+
if (boxId === "" !== (boxToken === "")) {
|
|
326
|
+
throw new Error("cloud session names a box with both its id and its token");
|
|
327
|
+
}
|
|
328
|
+
const session = {
|
|
329
|
+
version: CLOUD_SESSION_VERSION,
|
|
330
|
+
service: "sim",
|
|
331
|
+
simId: input.simId.trim(),
|
|
332
|
+
apiOrigin: normalizeCloudOrigin(input.apiOrigin.trim()),
|
|
333
|
+
claimId: input.claimId.trim(),
|
|
334
|
+
token: input.token,
|
|
335
|
+
...input.streamUrl ? { streamUrl: input.streamUrl } : {},
|
|
336
|
+
...input.platform ? { platform: input.platform } : {},
|
|
337
|
+
...input.device?.trim() ? { device: input.device.trim() } : {},
|
|
338
|
+
...input.artifact ? { artifact: input.artifact } : {},
|
|
339
|
+
...boxId && boxToken ? { boxId, boxToken } : {}
|
|
340
|
+
};
|
|
341
|
+
if (!session.simId || !session.claimId || !session.token) {
|
|
342
|
+
throw new Error("cloud session requires a simulator, an origin, a claim, and a token");
|
|
343
|
+
}
|
|
344
|
+
return session;
|
|
345
|
+
}
|
|
346
|
+
function parseCloudSession(encoded) {
|
|
347
|
+
const value = encoded.trim();
|
|
348
|
+
if (!value || value.length > MAX_ENCODED_SESSION_BYTES || !/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
349
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`);
|
|
350
|
+
}
|
|
351
|
+
let parsed;
|
|
352
|
+
try {
|
|
353
|
+
const bytes = Buffer.from(value, "base64url");
|
|
354
|
+
if (bytes.toString("base64url") !== value) {
|
|
355
|
+
throw new Error("non-canonical cloud session");
|
|
356
|
+
}
|
|
357
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
358
|
+
} catch {
|
|
359
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`);
|
|
360
|
+
}
|
|
361
|
+
if (!isRecord(parsed) || parsed.version !== CLOUD_SESSION_VERSION) {
|
|
362
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`);
|
|
363
|
+
}
|
|
364
|
+
const apiOrigin = parsed.apiOrigin;
|
|
365
|
+
if (typeof apiOrigin !== "string") {
|
|
366
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`);
|
|
367
|
+
}
|
|
368
|
+
const platform = parsed.platform === "ios" || parsed.platform === "android" ? parsed.platform : void 0;
|
|
369
|
+
const device = typeof parsed.device === "string" ? parsed.device : void 0;
|
|
370
|
+
const artifact = isRecord(parsed.artifact) && typeof parsed.artifact.sha256 === "string" ? {
|
|
371
|
+
sha256: parsed.artifact.sha256,
|
|
372
|
+
bytes: typeof parsed.artifact.bytes === "number" ? parsed.artifact.bytes : void 0
|
|
373
|
+
} : void 0;
|
|
374
|
+
const streamUrl = typeof parsed.streamUrl === "string" ? parsed.streamUrl : void 0;
|
|
375
|
+
const boxId = typeof parsed.boxId === "string" ? parsed.boxId : void 0;
|
|
376
|
+
const boxToken = typeof parsed.boxToken === "string" ? parsed.boxToken : void 0;
|
|
377
|
+
try {
|
|
378
|
+
const simId = parsed.simId;
|
|
379
|
+
const claimId = parsed.claimId;
|
|
380
|
+
const token = parsed.token;
|
|
381
|
+
if (parsed.service !== "sim" || typeof simId !== "string" || typeof claimId !== "string" || typeof token !== "string") {
|
|
382
|
+
throw new Error("unreadable cloud session");
|
|
383
|
+
}
|
|
384
|
+
return createCloudSession({
|
|
385
|
+
simId,
|
|
386
|
+
apiOrigin,
|
|
387
|
+
claimId,
|
|
388
|
+
token,
|
|
389
|
+
streamUrl,
|
|
390
|
+
platform,
|
|
391
|
+
device,
|
|
392
|
+
artifact,
|
|
393
|
+
boxId,
|
|
394
|
+
boxToken
|
|
395
|
+
});
|
|
396
|
+
} catch {
|
|
397
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function readCloudSession(environment = process.env) {
|
|
401
|
+
const encoded = environment[RNX_CLOUD_SESSION_ENV];
|
|
402
|
+
return encoded?.trim() ? parseCloudSession(encoded) : null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// cli/cloud-client.ts
|
|
406
|
+
var DEFAULT_RNX_CLOUD_ORIGIN = rnxPublicBrand.origin;
|
|
407
|
+
function isRecord2(value) {
|
|
408
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
409
|
+
}
|
|
410
|
+
function failureSentence(text) {
|
|
411
|
+
let parsed;
|
|
412
|
+
try {
|
|
413
|
+
parsed = JSON.parse(text);
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
if (!isRecord2(parsed)) return null;
|
|
418
|
+
if (isRecord2(parsed.error) && typeof parsed.error.message === "string") {
|
|
419
|
+
const code = typeof parsed.error.code === "string" && parsed.error.code ? ` [${parsed.error.code}]` : "";
|
|
420
|
+
return `${parsed.error.message}${code}`;
|
|
421
|
+
}
|
|
422
|
+
if (typeof parsed.message === "string") return parsed.message;
|
|
423
|
+
if (typeof parsed.error === "string") return parsed.error;
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
var ACCOUNT_CHOICE_REFUSALS = /* @__PURE__ */ new Set([
|
|
427
|
+
"accountId is required",
|
|
428
|
+
"you do not have access to this account"
|
|
429
|
+
]);
|
|
430
|
+
function responseError(status, text) {
|
|
431
|
+
const parsed = text ? failureSentence(text) : null;
|
|
432
|
+
const sentence = parsed && ACCOUNT_CHOICE_REFUSALS.has(parsed) ? CHOOSE_ACCOUNT_HINT : parsed;
|
|
433
|
+
return new Error(
|
|
434
|
+
`remote simulator request failed (${status})${sentence ? `: ${sentence}` : text.trim() ? `: ${text.trim()}` : ""}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
function resolveCloudSessionForParsed(parsed) {
|
|
438
|
+
const session = readCloudSession();
|
|
439
|
+
if (!session) return null;
|
|
440
|
+
if (parsed.simIdSource === "flag" && parsed.simId !== session.simId) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`this shell is connected to remote simulator ${session.simId}; ${parsed.simId ? `--sim ${parsed.simId}` : "the requested simulator"} is not available in this shell`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return session;
|
|
446
|
+
}
|
|
447
|
+
function cloudCommand(command) {
|
|
448
|
+
const clean = {};
|
|
449
|
+
for (const [key, value] of Object.entries(command)) {
|
|
450
|
+
if (key !== "id" && key !== "simId") clean[key] = value;
|
|
451
|
+
}
|
|
452
|
+
if (!isRnxCloudCommandType(command.type)) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`a remote simulator does not support bridge command type ${command.type}`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (!rnxCloudScopesAllowCommand(RNX_CLOUD_TOKEN_SCOPES, command.type)) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`a remote simulator command ${command.type} requires the ${rnxCloudCommandScope(command.type)} scope`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return clean;
|
|
463
|
+
}
|
|
464
|
+
var CloudBridge = class {
|
|
465
|
+
constructor(session) {
|
|
466
|
+
this.session = session;
|
|
467
|
+
}
|
|
468
|
+
plane = "cloud";
|
|
469
|
+
async send(command, options) {
|
|
470
|
+
const body = {
|
|
471
|
+
id: (0, import_node_crypto.randomUUID)(),
|
|
472
|
+
claimId: this.session.claimId,
|
|
473
|
+
command: cloudCommand(command)
|
|
474
|
+
};
|
|
475
|
+
let response;
|
|
476
|
+
try {
|
|
477
|
+
response = await fetch(
|
|
478
|
+
`${this.session.apiOrigin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`,
|
|
479
|
+
{
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: {
|
|
482
|
+
authorization: `Bearer ${this.session.token}`,
|
|
483
|
+
"content-type": "application/json"
|
|
484
|
+
},
|
|
485
|
+
body: JSON.stringify(body),
|
|
486
|
+
...options?.timeoutMs ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`could not reach the remote simulator at ${this.session.apiOrigin}: ${error instanceof Error ? error.message : String(error)}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
const text = await response.text();
|
|
495
|
+
if (!response.ok) throw responseError(response.status, text);
|
|
496
|
+
let parsed;
|
|
497
|
+
try {
|
|
498
|
+
parsed = JSON.parse(text);
|
|
499
|
+
} catch {
|
|
500
|
+
throw new Error("remote simulator command returned unreadable JSON");
|
|
501
|
+
}
|
|
502
|
+
if (!isRecord2(parsed) || parsed.id !== body.id) {
|
|
503
|
+
throw new Error("remote simulator command returned an invalid command receipt");
|
|
504
|
+
}
|
|
505
|
+
if (typeof parsed.error === "string" && parsed.error) throw new Error(parsed.error);
|
|
506
|
+
if (!Object.hasOwn(parsed, "result")) {
|
|
507
|
+
throw new Error("remote simulator command receipt has no result");
|
|
508
|
+
}
|
|
509
|
+
return parsed.result;
|
|
510
|
+
}
|
|
511
|
+
listSims() {
|
|
512
|
+
return Promise.reject(
|
|
513
|
+
new Error("a remote simulator does not expose a local simulator list")
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
focusSim() {
|
|
517
|
+
return Promise.reject(new Error("remote simulators have no browser window to focus"));
|
|
518
|
+
}
|
|
519
|
+
resolveReloadedSim() {
|
|
520
|
+
return Promise.resolve(null);
|
|
521
|
+
}
|
|
522
|
+
closeSim() {
|
|
523
|
+
return Promise.reject(
|
|
524
|
+
new Error("remote simulator close uses the simulator DELETE route")
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
claim() {
|
|
528
|
+
return Promise.reject(
|
|
529
|
+
new Error("remote simulator claim uses the simulator claim route")
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
close() {
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
function createCloudBridgeForParsed(parsed) {
|
|
536
|
+
const session = resolveCloudSessionForParsed(parsed);
|
|
537
|
+
if (!session) return null;
|
|
538
|
+
return new CloudBridge(session);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// cli/current-sim.ts
|
|
542
|
+
var import_fs2 = require("fs");
|
|
543
|
+
var import_os = require("os");
|
|
544
|
+
var import_path = require("path");
|
|
545
|
+
|
|
546
|
+
// cli/parent-pid.ts
|
|
547
|
+
var import_child_process = require("child_process");
|
|
548
|
+
var import_fs = require("fs");
|
|
549
|
+
var import_module = require("module");
|
|
550
|
+
var cached = null;
|
|
551
|
+
var cachedComm = null;
|
|
552
|
+
function getParentPid(pid) {
|
|
553
|
+
if (!cached) cached = build();
|
|
554
|
+
try {
|
|
555
|
+
return cached(pid);
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function getProcessComm(pid) {
|
|
561
|
+
if (!cachedComm) cachedComm = buildComm();
|
|
562
|
+
try {
|
|
563
|
+
return cachedComm(pid);
|
|
564
|
+
} catch {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function buildComm() {
|
|
569
|
+
if (process.platform === "linux") {
|
|
570
|
+
return (pid) => {
|
|
571
|
+
try {
|
|
572
|
+
return (0, import_fs.readFileSync)(`/proc/${pid}/comm`, "utf8").trim() || null;
|
|
573
|
+
} catch {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (process.platform === "darwin") {
|
|
579
|
+
const ffi = tryBuildDarwinCommFfi();
|
|
580
|
+
if (ffi) return ffi;
|
|
581
|
+
return (pid) => readProcessTable().get(pid)?.comm || null;
|
|
582
|
+
}
|
|
583
|
+
return () => null;
|
|
584
|
+
}
|
|
585
|
+
function build() {
|
|
586
|
+
const envPpid = Number(process.env.PPID);
|
|
587
|
+
if (Number.isFinite(envPpid) && envPpid > 1 && envPpid !== process.ppid) {
|
|
588
|
+
return () => envPpid;
|
|
589
|
+
}
|
|
590
|
+
if (process.platform === "linux") return buildLinux();
|
|
591
|
+
if (process.platform === "darwin") {
|
|
592
|
+
const ffi = tryBuildDarwinFfi();
|
|
593
|
+
if (ffi) return ffi;
|
|
594
|
+
return (pid) => {
|
|
595
|
+
const ppid = readProcessTable().get(pid)?.ppid;
|
|
596
|
+
return ppid !== void 0 && ppid > 0 ? ppid : null;
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return () => null;
|
|
600
|
+
}
|
|
601
|
+
var PROCESS_TABLE_TTL_MS = 2e3;
|
|
602
|
+
var processTable = { readAt: 0, rows: /* @__PURE__ */ new Map() };
|
|
603
|
+
function readProcessTable() {
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
if (processTable.readAt && now - processTable.readAt < PROCESS_TABLE_TTL_MS) {
|
|
606
|
+
return processTable.rows;
|
|
607
|
+
}
|
|
608
|
+
const rows = /* @__PURE__ */ new Map();
|
|
609
|
+
try {
|
|
610
|
+
const out = (0, import_child_process.execFileSync)("ps", ["-eo", "pid=,ppid=,comm="], {
|
|
611
|
+
encoding: "utf8",
|
|
612
|
+
timeout: 5e3,
|
|
613
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
614
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
615
|
+
});
|
|
616
|
+
for (const line of out.split("\n")) {
|
|
617
|
+
const parsed = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
618
|
+
if (!parsed) continue;
|
|
619
|
+
rows.set(Number(parsed[1]), {
|
|
620
|
+
ppid: Number(parsed[2]),
|
|
621
|
+
comm: (parsed[3].split("/").at(-1) ?? "").slice(0, PBI_COMM_LEN)
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
processTable = { readAt: now, rows };
|
|
627
|
+
return rows;
|
|
628
|
+
}
|
|
629
|
+
function buildLinux() {
|
|
630
|
+
return (pid) => {
|
|
631
|
+
try {
|
|
632
|
+
const raw = (0, import_fs.readFileSync)(`/proc/${pid}/stat`, "utf8");
|
|
633
|
+
const end = raw.lastIndexOf(")");
|
|
634
|
+
if (end < 0) return null;
|
|
635
|
+
const rest = raw.slice(end + 1).trim().split(/\s+/);
|
|
636
|
+
const ppid = Number(rest[1]);
|
|
637
|
+
return Number.isFinite(ppid) && ppid > 0 ? ppid : null;
|
|
638
|
+
} catch {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
var PROC_PIDTBSDINFO = 3;
|
|
644
|
+
var PROC_BSDINFO_SIZE = 216;
|
|
645
|
+
var PBI_PPID_OFFSET = 16;
|
|
646
|
+
var PBI_COMM_OFFSET = 48;
|
|
647
|
+
var PBI_COMM_LEN = 16;
|
|
648
|
+
function tryBuildDarwinFfi() {
|
|
649
|
+
if (!process.versions?.bun) return null;
|
|
650
|
+
try {
|
|
651
|
+
const r = (0, import_module.createRequire)(__sootsim_import_meta_url);
|
|
652
|
+
const { dlopen, FFIType } = r("bun:ffi");
|
|
653
|
+
const lib = dlopen("/usr/lib/libproc.dylib", {
|
|
654
|
+
proc_pidinfo: {
|
|
655
|
+
args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
|
|
656
|
+
returns: FFIType.i32
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
const buf = Buffer.alloc(PROC_BSDINFO_SIZE);
|
|
660
|
+
return (pid) => {
|
|
661
|
+
const ret = Number(
|
|
662
|
+
lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE)
|
|
663
|
+
);
|
|
664
|
+
if (ret <= 0) return null;
|
|
665
|
+
const ppid = buf.readUInt32LE(PBI_PPID_OFFSET);
|
|
666
|
+
return ppid > 0 ? ppid : null;
|
|
667
|
+
};
|
|
668
|
+
} catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function tryBuildDarwinCommFfi() {
|
|
673
|
+
if (!process.versions?.bun) return null;
|
|
674
|
+
try {
|
|
675
|
+
const r = (0, import_module.createRequire)(__sootsim_import_meta_url);
|
|
676
|
+
const { dlopen, FFIType } = r("bun:ffi");
|
|
677
|
+
const lib = dlopen("/usr/lib/libproc.dylib", {
|
|
678
|
+
proc_pidinfo: {
|
|
679
|
+
args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
|
|
680
|
+
returns: FFIType.i32
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
const buf = Buffer.alloc(PROC_BSDINFO_SIZE);
|
|
684
|
+
return (pid) => {
|
|
685
|
+
const ret = Number(
|
|
686
|
+
lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE)
|
|
687
|
+
);
|
|
688
|
+
if (ret <= 0) return null;
|
|
689
|
+
const raw = buf.subarray(PBI_COMM_OFFSET, PBI_COMM_OFFSET + PBI_COMM_LEN);
|
|
690
|
+
const end = raw.indexOf(0);
|
|
691
|
+
const comm = raw.subarray(0, end < 0 ? raw.length : end).toString("utf8");
|
|
692
|
+
return comm || null;
|
|
693
|
+
};
|
|
694
|
+
} catch {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// cli/secure-file.ts
|
|
700
|
+
var import_node_crypto2 = require("node:crypto");
|
|
701
|
+
var import_node_fs2 = require("node:fs");
|
|
702
|
+
var import_node_path2 = require("node:path");
|
|
703
|
+
function writeSecureJsonAtomic(filepath, value) {
|
|
704
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(filepath), { recursive: true });
|
|
705
|
+
const temporary = `${filepath}.tmp-${process.pid}-${(0, import_node_crypto2.randomUUID)()}`;
|
|
706
|
+
let descriptor = null;
|
|
707
|
+
try {
|
|
708
|
+
descriptor = (0, import_node_fs2.openSync)(temporary, "wx", 384);
|
|
709
|
+
(0, import_node_fs2.fchmodSync)(descriptor, 384);
|
|
710
|
+
(0, import_node_fs2.writeFileSync)(descriptor, `${JSON.stringify(value, null, 2)}
|
|
711
|
+
`);
|
|
712
|
+
(0, import_node_fs2.fsyncSync)(descriptor);
|
|
713
|
+
(0, import_node_fs2.closeSync)(descriptor);
|
|
714
|
+
descriptor = null;
|
|
715
|
+
(0, import_node_fs2.renameSync)(temporary, filepath);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (descriptor !== null) (0, import_node_fs2.closeSync)(descriptor);
|
|
718
|
+
(0, import_node_fs2.rmSync)(temporary, { force: true });
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// cli/current-sim.ts
|
|
724
|
+
var CURRENT_SIM_VERSION = 2;
|
|
725
|
+
var CURRENT_SIM_PATH_ENV = "SOOTSIM_CLI_CURRENT_SIM_PATH";
|
|
726
|
+
var IDENTITY_ENV_VARS = [
|
|
727
|
+
"RNX_CLI_IDENTITY",
|
|
728
|
+
// explicit override — fastest path, skips all pid lookups
|
|
729
|
+
"TM_SESSION",
|
|
730
|
+
// Team Machine session id; routes prompt-bar followups back to this agent
|
|
731
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
732
|
+
// claude code, if ever set
|
|
733
|
+
"CODEX_THREAD_ID",
|
|
734
|
+
// codex / openai agent thread, stable across tool invocations
|
|
735
|
+
"TERM_SESSION_ID",
|
|
736
|
+
// macOS Terminal.app + iTerm2
|
|
737
|
+
"ITERM_SESSION_ID",
|
|
738
|
+
// iTerm2 (older variant)
|
|
739
|
+
"TMUX_PANE",
|
|
740
|
+
// tmux — stable per pane
|
|
741
|
+
"STY",
|
|
742
|
+
// GNU screen session
|
|
743
|
+
"KITTY_WINDOW_ID",
|
|
744
|
+
// kitty
|
|
745
|
+
"WEZTERM_PANE",
|
|
746
|
+
// wezterm
|
|
747
|
+
"ALACRITTY_WINDOW_ID",
|
|
748
|
+
// alacritty
|
|
749
|
+
"WINDOWID",
|
|
750
|
+
// X11 terminals
|
|
751
|
+
"VSCODE_INJECTION"
|
|
752
|
+
// vscode integrated terminal
|
|
753
|
+
];
|
|
754
|
+
var IMMORTAL_HOST_COMM_RE = /^(?:tmux|screen)|^tm$/i;
|
|
755
|
+
function findTopAncestor(start, maxDepth = 20) {
|
|
756
|
+
let prev = start;
|
|
757
|
+
let pid = getParentPid(start);
|
|
758
|
+
if (!pid || pid <= 1) {
|
|
759
|
+
const comm = getProcessComm(start);
|
|
760
|
+
return start > 1 && comm && !IMMORTAL_HOST_COMM_RE.test(comm) ? start : null;
|
|
761
|
+
}
|
|
762
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
763
|
+
const comm = getProcessComm(pid);
|
|
764
|
+
if (comm && IMMORTAL_HOST_COMM_RE.test(comm)) return prev;
|
|
765
|
+
const parent = getParentPid(pid);
|
|
766
|
+
if (!parent || parent <= 1) {
|
|
767
|
+
return comm ? pid : prev;
|
|
768
|
+
}
|
|
769
|
+
prev = pid;
|
|
770
|
+
pid = parent;
|
|
771
|
+
}
|
|
772
|
+
return pid;
|
|
773
|
+
}
|
|
774
|
+
function getCliIdentity() {
|
|
775
|
+
for (const name2 of IDENTITY_ENV_VARS) {
|
|
776
|
+
const value = process.env[name2];
|
|
777
|
+
if (value && value.trim()) {
|
|
778
|
+
return { key: `${name2}:${value.trim()}`, source: name2, stable: true };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const topPid = findTopAncestor(process.ppid);
|
|
782
|
+
if (topPid && topPid > 1) {
|
|
783
|
+
return { key: `gppid-${topPid}`, source: "grand-ppid", stable: true };
|
|
784
|
+
}
|
|
785
|
+
return { key: `pid-${process.ppid}`, source: "ppid", stable: false };
|
|
786
|
+
}
|
|
787
|
+
function getCliIdentityKey() {
|
|
788
|
+
return getCliIdentity().key;
|
|
789
|
+
}
|
|
790
|
+
function getCurrentSimPath() {
|
|
791
|
+
if (process.env[CURRENT_SIM_PATH_ENV]) {
|
|
792
|
+
return (0, import_path.resolve)(process.env[CURRENT_SIM_PATH_ENV]);
|
|
793
|
+
}
|
|
794
|
+
const key = getCliIdentityKey();
|
|
795
|
+
return (0, import_path.join)((0, import_os.tmpdir)(), `rnx-current-sim-${key}.json`);
|
|
796
|
+
}
|
|
797
|
+
function legacyCloudCredentialDirectory(filepath) {
|
|
798
|
+
return (0, import_path.join)((0, import_path.dirname)(filepath), `${(0, import_path.basename)(filepath)}.credentials`);
|
|
799
|
+
}
|
|
800
|
+
function readCurrentTarget() {
|
|
801
|
+
const filepath = getCurrentSimPath();
|
|
802
|
+
(0, import_fs2.rmSync)(legacyCloudCredentialDirectory(filepath), { recursive: true, force: true });
|
|
803
|
+
if (!(0, import_fs2.existsSync)(filepath)) return null;
|
|
804
|
+
try {
|
|
805
|
+
const parsed = JSON.parse((0, import_fs2.readFileSync)(filepath, "utf8"));
|
|
806
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
807
|
+
(0, import_fs2.rmSync)(filepath, { force: true });
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
const version = Reflect.get(parsed, "version");
|
|
811
|
+
const plane = Reflect.get(parsed, "plane");
|
|
812
|
+
const simId = Reflect.get(parsed, "simId");
|
|
813
|
+
const updatedAt = Reflect.get(parsed, "updatedAt");
|
|
814
|
+
if (version !== 1 && version !== CURRENT_SIM_VERSION || version === CURRENT_SIM_VERSION && plane !== "local" || typeof simId !== "string" || !simId.trim() || typeof updatedAt !== "string") {
|
|
815
|
+
(0, import_fs2.rmSync)(filepath, { force: true });
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
const target = {
|
|
819
|
+
version: CURRENT_SIM_VERSION,
|
|
820
|
+
plane: "local",
|
|
821
|
+
simId: simId.trim(),
|
|
822
|
+
updatedAt
|
|
823
|
+
};
|
|
824
|
+
if (version === 1) writeSecureJsonAtomic(filepath, target);
|
|
825
|
+
return target;
|
|
826
|
+
} catch {
|
|
827
|
+
(0, import_fs2.rmSync)(filepath, { force: true });
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function readCurrentSimId() {
|
|
832
|
+
return readCurrentTarget()?.simId ?? null;
|
|
833
|
+
}
|
|
834
|
+
function saveCurrentSimId(simId) {
|
|
835
|
+
const normalized = simId.trim();
|
|
836
|
+
if (!normalized) return;
|
|
837
|
+
writeSecureJsonAtomic(getCurrentSimPath(), {
|
|
838
|
+
version: CURRENT_SIM_VERSION,
|
|
839
|
+
plane: "local",
|
|
840
|
+
simId: normalized,
|
|
841
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function clearCurrentSimId() {
|
|
845
|
+
(0, import_fs2.rmSync)(getCurrentSimPath(), { force: true });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// cli/ws-bridge.ts
|
|
849
|
+
var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
850
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
851
|
+
var import_ws = require("ws");
|
|
852
|
+
|
|
853
|
+
// src/bridge-constants.ts
|
|
854
|
+
var DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
|
|
855
|
+
var DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/`;
|
|
856
|
+
function parsePort(value) {
|
|
857
|
+
if (typeof value === "number") {
|
|
858
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
859
|
+
}
|
|
860
|
+
if (typeof value !== "string") return null;
|
|
861
|
+
const trimmed = value.trim();
|
|
862
|
+
if (!trimmed) return null;
|
|
863
|
+
const parsed = Number(trimmed);
|
|
864
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
865
|
+
}
|
|
866
|
+
function parsePortOffset(value) {
|
|
867
|
+
const parsed = parsePort(value);
|
|
868
|
+
return parsed === null ? 0 : parsed;
|
|
869
|
+
}
|
|
870
|
+
function resolveSootsimBridgePort(input = {}) {
|
|
871
|
+
const explicitPort = parsePort(input.explicitPort);
|
|
872
|
+
if (explicitPort !== null) return explicitPort;
|
|
873
|
+
return DEFAULT_SOOTSIM_BRIDGE_PORT + parsePortOffset(input.portOffset);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/cli-constants.ts
|
|
877
|
+
var DEFAULT_SOOTSIM_SHELL_URL = "http://localhost:5173/";
|
|
878
|
+
|
|
879
|
+
// cli/ws-bridge.ts
|
|
880
|
+
function bridgeWsUrl(wsPort) {
|
|
881
|
+
return `ws://127.0.0.1:${wsPort}`;
|
|
882
|
+
}
|
|
883
|
+
var resolvedWorld = null;
|
|
884
|
+
function liveDevBridgeLockfiles() {
|
|
885
|
+
return readDevBridgeLockfiles().filter((lockfile) => isDevBridgeLockfileFresh(lockfile));
|
|
886
|
+
}
|
|
887
|
+
function checkoutRoot(cwd) {
|
|
888
|
+
let current = import_node_path3.default.resolve(cwd);
|
|
889
|
+
while (true) {
|
|
890
|
+
if (import_node_fs3.default.existsSync(import_node_path3.default.join(current, ".git"))) return current;
|
|
891
|
+
const parent = import_node_path3.default.dirname(current);
|
|
892
|
+
if (parent === current) return null;
|
|
893
|
+
current = parent;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function pathContains(parent, child) {
|
|
897
|
+
const relative2 = import_node_path3.default.relative(parent, child);
|
|
898
|
+
return relative2 === "" || relative2 !== ".." && !relative2.startsWith(`..${import_node_path3.default.sep}`) && !import_node_path3.default.isAbsolute(relative2);
|
|
899
|
+
}
|
|
900
|
+
function resolveDefaultDevBridgeLockfile() {
|
|
901
|
+
const lockfiles = liveDevBridgeLockfiles();
|
|
902
|
+
if (lockfiles.length === 0) return null;
|
|
903
|
+
if (process.env.SOOTSIM_BRIDGE_PORT !== void 0 || process.env.PORT_OFFSET !== void 0) {
|
|
904
|
+
const requestedPort = resolveSootsimBridgePort({
|
|
905
|
+
explicitPort: process.env.SOOTSIM_BRIDGE_PORT,
|
|
906
|
+
portOffset: process.env.PORT_OFFSET
|
|
907
|
+
});
|
|
908
|
+
const requested = lockfiles.find((lockfile) => lockfile.bridgePort === requestedPort);
|
|
909
|
+
if (requested) return requested;
|
|
910
|
+
}
|
|
911
|
+
let cwd = null;
|
|
912
|
+
try {
|
|
913
|
+
cwd = process.cwd();
|
|
914
|
+
} catch {
|
|
915
|
+
}
|
|
916
|
+
if (cwd) {
|
|
917
|
+
const currentCheckout = checkoutRoot(cwd);
|
|
918
|
+
const sameCheckout = lockfiles.filter((lockfile) => {
|
|
919
|
+
const bridgeCheckout = checkoutRoot(lockfile.cwd);
|
|
920
|
+
if (currentCheckout && bridgeCheckout) return currentCheckout === bridgeCheckout;
|
|
921
|
+
return pathContains(cwd, lockfile.cwd) || pathContains(lockfile.cwd, cwd);
|
|
922
|
+
});
|
|
923
|
+
if (sameCheckout.length > 0) {
|
|
924
|
+
return sameCheckout.find(
|
|
925
|
+
(lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT
|
|
926
|
+
) ?? sameCheckout[0];
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ?? lockfiles[0];
|
|
930
|
+
}
|
|
931
|
+
function resolveBridgeWorld() {
|
|
932
|
+
const homeDir = rnxHomeDir();
|
|
933
|
+
if (resolvedWorld?.homeDir === homeDir) return resolvedWorld.world;
|
|
934
|
+
const world = resolveBridgeWorldFresh();
|
|
935
|
+
resolvedWorld = { homeDir, world };
|
|
936
|
+
return world;
|
|
937
|
+
}
|
|
938
|
+
function resolveBridgeWorldFresh() {
|
|
939
|
+
const dev = resolveDefaultDevBridgeLockfile();
|
|
940
|
+
if (dev) {
|
|
941
|
+
return {
|
|
942
|
+
source: "dev-bridge",
|
|
943
|
+
bridgePort: dev.bridgePort,
|
|
944
|
+
// older lockfile writers don't record the vite http port; the default
|
|
945
|
+
// shell URL is the dev shell anyway, so the fallback stays coherent.
|
|
946
|
+
shellBaseUrl: dev.shellPort ? `http://localhost:${dev.shellPort}/` : DEFAULT_SOOTSIM_SHELL_URL
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const lock = readDaemonLockfile();
|
|
950
|
+
if (lock && isDaemonLockfileFresh(lock)) {
|
|
951
|
+
return {
|
|
952
|
+
source: "daemon",
|
|
953
|
+
bridgePort: lock.bridgePort,
|
|
954
|
+
shellBaseUrl: lock.runtimePort > 0 ? `http://localhost:${lock.runtimePort}/` : DEFAULT_SOOTSIM_SHELL_URL
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
source: "default",
|
|
959
|
+
bridgePort: DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
960
|
+
shellBaseUrl: DEFAULT_SOOTSIM_SHELL_URL
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
var BridgeSimLockedError = class extends Error {
|
|
964
|
+
lock;
|
|
965
|
+
constructor(message, lock) {
|
|
966
|
+
super(message);
|
|
967
|
+
this.name = "BridgeSimLockedError";
|
|
968
|
+
this.lock = lock;
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
function describeSimIdSource(source) {
|
|
972
|
+
if (source === "flag") return "via --sim";
|
|
973
|
+
if (source === "saved") return "saved via `rnx use`";
|
|
974
|
+
return "primary fallback \u2014 no sim pinned";
|
|
975
|
+
}
|
|
976
|
+
var BRIDGE_VALUE_FLAGS_TABLE = [
|
|
977
|
+
{
|
|
978
|
+
names: ["--port", "-p"],
|
|
979
|
+
// naming the flag pins the world even when the value is missing or junk,
|
|
980
|
+
// so a later --base-url cannot quietly retarget the run to another shell.
|
|
981
|
+
apply: (state, value) => {
|
|
982
|
+
state.explicitPort = true;
|
|
983
|
+
if (value !== void 0) state.wsPort = Number(value);
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
names: ["--timeout"],
|
|
988
|
+
apply: (state, value) => {
|
|
989
|
+
if (value !== void 0) state.commandTimeoutMs = Number(value);
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
// `--sim` is the canonical target flag; `--session` / `--tab` are
|
|
994
|
+
// documented aliases (CLAUDE.md, the rnx agent skills, and engine docs all
|
|
995
|
+
// say `--session <id>`). without these aliases an agent following the docs
|
|
996
|
+
// would have `--session ef` silently ignored and get the *currently
|
|
997
|
+
// pinned* sim's tree instead — wrong-app data with no error.
|
|
998
|
+
names: ["--sim", "--session", "--tab"],
|
|
999
|
+
apply: (state, value) => {
|
|
1000
|
+
if (value === void 0) return;
|
|
1001
|
+
state.simId = value.trim() || void 0;
|
|
1002
|
+
state.simIdSource = "flag";
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
];
|
|
1006
|
+
var BRIDGE_VALUE_FLAGS = BRIDGE_VALUE_FLAGS_TABLE.flatMap(
|
|
1007
|
+
(flag) => [...flag.names]
|
|
1008
|
+
);
|
|
1009
|
+
function createBridge(wsPort, opts = {}) {
|
|
1010
|
+
let nextId = 1;
|
|
1011
|
+
const commandTimeoutMs = opts.commandTimeoutMs ?? 15e3;
|
|
1012
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1013
|
+
const ws = new import_ws.WebSocket(bridgeWsUrl(wsPort));
|
|
1014
|
+
const identity = getCliIdentity();
|
|
1015
|
+
const ready = new Promise((resolve3, reject) => {
|
|
1016
|
+
ws.on("open", () => {
|
|
1017
|
+
try {
|
|
1018
|
+
ws.send(
|
|
1019
|
+
JSON.stringify({
|
|
1020
|
+
type: "bridge:hello",
|
|
1021
|
+
id: 0,
|
|
1022
|
+
cliIdentityKey: identity.key,
|
|
1023
|
+
cliIdentitySource: identity.source,
|
|
1024
|
+
cliLabel: opts.cliLabel
|
|
1025
|
+
})
|
|
1026
|
+
);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
resolve3();
|
|
1030
|
+
});
|
|
1031
|
+
ws.on("error", (err) => {
|
|
1032
|
+
const cause = err.message ? `: ${err.message}` : "";
|
|
1033
|
+
reject(new Error(`could not connect to ${bridgeWsUrl(wsPort)}${cause}`));
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
ready.catch(() => {
|
|
1037
|
+
});
|
|
1038
|
+
let warnedAboutContention = false;
|
|
1039
|
+
let noticedTarget = false;
|
|
1040
|
+
const lineageBySimId = /* @__PURE__ */ new Map();
|
|
1041
|
+
const lineageLookupBySimId = /* @__PURE__ */ new Map();
|
|
1042
|
+
function pickReloadedTarget(sims, retiredSimId) {
|
|
1043
|
+
const lineage = lineageBySimId.get(retiredSimId);
|
|
1044
|
+
if (!lineage) return null;
|
|
1045
|
+
const matches = (Array.isArray(sims) ? sims : []).filter(
|
|
1046
|
+
(sim) => sim.readyState === "open" && sim.meta?.sootsimHostPid === lineage.hostPid && sim.connectedAt > lineage.connectedAt
|
|
1047
|
+
);
|
|
1048
|
+
const successor = matches.find((sim) => sim.id === retiredSimId) ?? (matches.length === 1 ? matches[0] : null);
|
|
1049
|
+
if (!successor) return null;
|
|
1050
|
+
lineageBySimId.set(successor.id, {
|
|
1051
|
+
hostPid: lineage.hostPid,
|
|
1052
|
+
connectedAt: successor.connectedAt
|
|
1053
|
+
});
|
|
1054
|
+
return successor.id;
|
|
1055
|
+
}
|
|
1056
|
+
function maybeNoticeTarget(cmdType, simId) {
|
|
1057
|
+
if (noticedTarget) return;
|
|
1058
|
+
if (process.env.SOOTSIM_QUIET_TARGET_NOTICE === "1") return;
|
|
1059
|
+
if (cmdType.startsWith("bridge:") || cmdType === "focus" || cmdType === "close")
|
|
1060
|
+
return;
|
|
1061
|
+
noticedTarget = true;
|
|
1062
|
+
const label = simId ?? "primary";
|
|
1063
|
+
const source = describeSimIdSource(simId ? opts.simIdSource : "none");
|
|
1064
|
+
process.stderr.write(` \u2192 ${label} (${source})
|
|
1065
|
+
`);
|
|
1066
|
+
}
|
|
1067
|
+
ws.on("message", (data) => {
|
|
1068
|
+
let msg;
|
|
1069
|
+
try {
|
|
1070
|
+
msg = JSON.parse(data.toString());
|
|
1071
|
+
} catch {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (msg.id === 0) return;
|
|
1075
|
+
const p = pending.get(msg.id);
|
|
1076
|
+
if (!p) return;
|
|
1077
|
+
pending.delete(msg.id);
|
|
1078
|
+
if (msg._otherCliCount > 0 && !warnedAboutContention) {
|
|
1079
|
+
warnedAboutContention = true;
|
|
1080
|
+
process.stderr.write(
|
|
1081
|
+
`
|
|
1082
|
+
\u26A0 ${msg._otherCliCount} other CLI identity/identities are driving this sim
|
|
1083
|
+
taps, scrolls, and keyboard input from multiple agents will collide on
|
|
1084
|
+
the same screen state (this is not a bridge throughput limit).
|
|
1085
|
+
use \`rnx open --new\` for an isolated sim per agent.
|
|
1086
|
+
|
|
1087
|
+
`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (msg.error) {
|
|
1091
|
+
if (msg._locked) {
|
|
1092
|
+
p.reject(new BridgeSimLockedError(msg.error, msg._locked));
|
|
1093
|
+
} else {
|
|
1094
|
+
p.reject(new Error(msg.error));
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
p.resolve(msg.result);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
ws.on("close", (code, reason) => {
|
|
1101
|
+
const msg = reason?.toString() || "connection closed";
|
|
1102
|
+
for (const [id, p] of pending) {
|
|
1103
|
+
pending.delete(id);
|
|
1104
|
+
p.reject(new Error(`sim disconnected: ${msg} (code ${code})`));
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
return {
|
|
1108
|
+
plane: "local",
|
|
1109
|
+
async send(cmd, callOpts) {
|
|
1110
|
+
const effectiveTimeoutMs = callOpts?.timeoutMs ?? commandTimeoutMs;
|
|
1111
|
+
const sendOnce = async (defaultSimId, cmdOverride) => {
|
|
1112
|
+
await ready;
|
|
1113
|
+
const id = nextId++;
|
|
1114
|
+
return new Promise((resolve3, reject) => {
|
|
1115
|
+
const timeout = setTimeout(() => {
|
|
1116
|
+
pending.delete(id);
|
|
1117
|
+
reject(
|
|
1118
|
+
new Error(
|
|
1119
|
+
`command timed out after ${Math.round(effectiveTimeoutMs / 1e3)}s`
|
|
1120
|
+
)
|
|
1121
|
+
);
|
|
1122
|
+
}, effectiveTimeoutMs);
|
|
1123
|
+
pending.set(id, {
|
|
1124
|
+
resolve: (v) => {
|
|
1125
|
+
clearTimeout(timeout);
|
|
1126
|
+
resolve3(v);
|
|
1127
|
+
},
|
|
1128
|
+
reject: (e) => {
|
|
1129
|
+
clearTimeout(timeout);
|
|
1130
|
+
reject(e);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
const payload = { ...cmdOverride ?? cmd, id };
|
|
1134
|
+
if (payload.simId === void 0 && defaultSimId) {
|
|
1135
|
+
payload.simId = defaultSimId;
|
|
1136
|
+
}
|
|
1137
|
+
maybeNoticeTarget(payload.type ?? "", payload.simId);
|
|
1138
|
+
ws.send(JSON.stringify(payload));
|
|
1139
|
+
});
|
|
1140
|
+
};
|
|
1141
|
+
const sentSimId = opts.simIdSource === "flag" ? opts.simId : readCurrentSimId() ?? opts.simId;
|
|
1142
|
+
const targetedSimId = typeof cmd.simId === "string" && cmd.simId ? cmd.simId : sentSimId;
|
|
1143
|
+
try {
|
|
1144
|
+
if (targetedSimId && cmd.type !== "bridge:list-sims" && !lineageBySimId.has(targetedSimId)) {
|
|
1145
|
+
let lookup = lineageLookupBySimId.get(targetedSimId);
|
|
1146
|
+
if (!lookup) {
|
|
1147
|
+
lookup = (async () => {
|
|
1148
|
+
try {
|
|
1149
|
+
const sims = await sendOnce(void 0, { type: "bridge:list-sims" });
|
|
1150
|
+
if (!Array.isArray(sims)) return;
|
|
1151
|
+
const targeted = sims.find(
|
|
1152
|
+
(sim) => sim.id === targetedSimId
|
|
1153
|
+
);
|
|
1154
|
+
const hostPid = targeted?.meta?.sootsimHostPid;
|
|
1155
|
+
if (targeted && typeof hostPid === "number") {
|
|
1156
|
+
lineageBySimId.set(targetedSimId, {
|
|
1157
|
+
hostPid,
|
|
1158
|
+
connectedAt: targeted.connectedAt
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
} catch {
|
|
1162
|
+
} finally {
|
|
1163
|
+
lineageLookupBySimId.delete(targetedSimId);
|
|
1164
|
+
}
|
|
1165
|
+
})();
|
|
1166
|
+
lineageLookupBySimId.set(targetedSimId, lookup);
|
|
1167
|
+
}
|
|
1168
|
+
await lookup;
|
|
1169
|
+
}
|
|
1170
|
+
return await sendOnce(sentSimId);
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1173
|
+
const missingSimMessage = `no sim connected with id ${sentSimId}`;
|
|
1174
|
+
if (opts.simIdSource !== "flag" && sentSimId && cmd.simId === void 0 && (message === missingSimMessage || message.startsWith(`${missingSimMessage};`))) {
|
|
1175
|
+
const successor = pickReloadedTarget(
|
|
1176
|
+
await sendOnce(void 0, { type: "bridge:list-sims" }),
|
|
1177
|
+
sentSimId
|
|
1178
|
+
);
|
|
1179
|
+
if (successor) {
|
|
1180
|
+
saveCurrentSimId(successor);
|
|
1181
|
+
process.stderr.write(
|
|
1182
|
+
` note: sim ${sentSimId} reloaded and reconnected as ${successor}; following it
|
|
1183
|
+
`
|
|
1184
|
+
);
|
|
1185
|
+
return sendOnce(successor);
|
|
1186
|
+
}
|
|
1187
|
+
clearCurrentSimId();
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`saved sim ${sentSimId} is gone${message.slice(missingSimMessage.length)}; run \`rnx list\` and \`rnx use <sim>\`, or open a fresh sim`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
throw error;
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
async listSims() {
|
|
1196
|
+
const sims = await this.send({ type: "bridge:list-sims" });
|
|
1197
|
+
return Array.isArray(sims) ? sims : [];
|
|
1198
|
+
},
|
|
1199
|
+
// a caller holding its own pinned sim id (a flow runner sets simId on every
|
|
1200
|
+
// command, so it never reaches the pin-following path in send) asks here
|
|
1201
|
+
// after a reload retires its socket. the page can reconnect under a new id
|
|
1202
|
+
// or restore the old id; null means no unique same-host target exists.
|
|
1203
|
+
async resolveReloadedSim(retiredSimId) {
|
|
1204
|
+
return pickReloadedTarget(await this.listSims(), retiredSimId);
|
|
1205
|
+
},
|
|
1206
|
+
async focusSim(simId) {
|
|
1207
|
+
return this.send({ type: "focus", simId });
|
|
1208
|
+
},
|
|
1209
|
+
async closeSim(simId) {
|
|
1210
|
+
return this.send({ type: "close", simId });
|
|
1211
|
+
},
|
|
1212
|
+
async claim(simId, claimOpts = {}) {
|
|
1213
|
+
const result = await this.send({
|
|
1214
|
+
type: "bridge:claim",
|
|
1215
|
+
simId,
|
|
1216
|
+
force: claimOpts.force === true
|
|
1217
|
+
});
|
|
1218
|
+
return result;
|
|
1219
|
+
},
|
|
1220
|
+
close() {
|
|
1221
|
+
try {
|
|
1222
|
+
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
1223
|
+
ws.send(JSON.stringify({ type: "bridge:bye", id: 0 }));
|
|
1224
|
+
}
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
ws.close();
|
|
1228
|
+
const terminateGrace = setTimeout(() => {
|
|
1229
|
+
if (ws.readyState !== import_ws.WebSocket.CLOSED) {
|
|
1230
|
+
ws.terminate();
|
|
1231
|
+
}
|
|
1232
|
+
}, 250);
|
|
1233
|
+
ws.once("close", () => clearTimeout(terminateGrace));
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/bridge-contract.ts
|
|
1239
|
+
var PUBLIC_NO_NETWORK_MARKER = "public RNX Cloud simulators have no network";
|
|
1240
|
+
var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
|
|
1241
|
+
|
|
1242
|
+
// cli/commands/inspect/core.ts
|
|
1243
|
+
async function inspectTree(bridge, depth = 5) {
|
|
1244
|
+
const result = await bridge.send({
|
|
1245
|
+
type: "query",
|
|
1246
|
+
query: { kind: "tree", depth }
|
|
1247
|
+
});
|
|
1248
|
+
const tree = isSemanticQueryResult(result) ? result.nodes : [];
|
|
1249
|
+
return { depth, tree };
|
|
1250
|
+
}
|
|
1251
|
+
async function inspectUrl(bridge) {
|
|
1252
|
+
const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
|
|
1253
|
+
return { url: typeof url === "string" ? url : "" };
|
|
1254
|
+
}
|
|
1255
|
+
function isSemanticGeometry(value) {
|
|
1256
|
+
return value !== null && typeof value === "object" && [
|
|
1257
|
+
Reflect.get(value, "x"),
|
|
1258
|
+
Reflect.get(value, "y"),
|
|
1259
|
+
Reflect.get(value, "width"),
|
|
1260
|
+
Reflect.get(value, "height")
|
|
1261
|
+
].every(
|
|
1262
|
+
(coordinate) => typeof coordinate === "number" ? Number.isFinite(coordinate) : false
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
function isSimSemanticTree(value) {
|
|
1266
|
+
if (!Array.isArray(value)) return false;
|
|
1267
|
+
return value.every((node) => {
|
|
1268
|
+
if (node === null || typeof node !== "object" || !Number.isInteger(Reflect.get(node, "nodeId")) || typeof Reflect.get(node, "type") !== "string" || !isSemanticGeometry(Reflect.get(node, "geometry"))) {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
for (const key of ["testID", "text", "role", "label"]) {
|
|
1272
|
+
const value2 = Reflect.get(node, key);
|
|
1273
|
+
if (value2 !== void 0 && typeof value2 !== "string") return false;
|
|
1274
|
+
}
|
|
1275
|
+
const pressable = Reflect.get(node, "pressable");
|
|
1276
|
+
if (pressable !== void 0 && pressable !== true) return false;
|
|
1277
|
+
const children = Reflect.get(node, "children");
|
|
1278
|
+
return children === void 0 || isSimSemanticTree(children);
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
function isSemanticQueryResult(value) {
|
|
1282
|
+
const total = value !== null && typeof value === "object" ? Reflect.get(value, "total") : void 0;
|
|
1283
|
+
return value !== null && typeof value === "object" && typeof total === "number" && Number.isInteger(total) && total >= 0 && isSimSemanticTree(Reflect.get(value, "nodes"));
|
|
1284
|
+
}
|
|
1285
|
+
function globRegex(glob) {
|
|
1286
|
+
const escaped = glob.replace(/[.+^$(){}|[\]\\]/g, "\\$&");
|
|
1287
|
+
return new RegExp(`^${escaped.replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
|
|
1288
|
+
}
|
|
1289
|
+
function formatSemanticTree(nodes, opts = {}) {
|
|
1290
|
+
const filter = opts.filter?.toLowerCase() ?? "";
|
|
1291
|
+
const testIDRegex = opts.testIdLike ? globRegex(opts.testIdLike) : null;
|
|
1292
|
+
const onlyRegex = opts.onlyGlob ? globRegex(opts.onlyGlob) : null;
|
|
1293
|
+
const lines = [];
|
|
1294
|
+
const visit = (node, indent) => {
|
|
1295
|
+
const joined = [node.role, node.label, node.text, node.testID].filter((value) => typeof value === "string").join(" ");
|
|
1296
|
+
const matches = (!filter || joined.toLowerCase().includes(filter)) && (!testIDRegex || testIDRegex.test(node.testID ?? "")) && (!onlyRegex || onlyRegex.test(joined));
|
|
1297
|
+
if (matches) {
|
|
1298
|
+
const parts = [
|
|
1299
|
+
node.role ? `[${node.role}]` : node.pressable ? "[tap]" : `<${node.type}>`
|
|
1300
|
+
];
|
|
1301
|
+
if (node.text) {
|
|
1302
|
+
const text = opts.fullText || node.text.length <= 50 ? node.text : `${node.text.slice(0, 49).trimEnd()}\u2026`;
|
|
1303
|
+
parts.push(JSON.stringify(text));
|
|
1304
|
+
}
|
|
1305
|
+
if (node.testID) parts.push(`#${node.testID}`);
|
|
1306
|
+
if (node.label && node.label !== node.text) {
|
|
1307
|
+
parts.push(`label=${JSON.stringify(node.label)}`);
|
|
1308
|
+
}
|
|
1309
|
+
if (!opts.hideXy) {
|
|
1310
|
+
const geometry = node.geometry;
|
|
1311
|
+
parts.push(
|
|
1312
|
+
`@(${Math.round(geometry.x)},${Math.round(geometry.y)}) ${Math.round(geometry.width)}x${Math.round(geometry.height)}`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
if (node.pressable) parts.push("(tap)");
|
|
1316
|
+
lines.push(`${" ".repeat(indent + 1)}${parts.join(" ")}`);
|
|
1317
|
+
}
|
|
1318
|
+
for (const child of node.children ?? []) visit(child, indent + 1);
|
|
1319
|
+
};
|
|
1320
|
+
let roots = nodes;
|
|
1321
|
+
if (opts.subtreeRoot) {
|
|
1322
|
+
const pending = [...nodes];
|
|
1323
|
+
let found;
|
|
1324
|
+
while (pending.length > 0 && !found) {
|
|
1325
|
+
const candidate = pending.shift();
|
|
1326
|
+
if (!candidate) continue;
|
|
1327
|
+
if (candidate.testID === opts.subtreeRoot) found = candidate;
|
|
1328
|
+
else pending.push(...candidate.children ?? []);
|
|
1329
|
+
}
|
|
1330
|
+
if (!found) return `__SUBTREE_NOT_FOUND__:${opts.subtreeRoot}`;
|
|
1331
|
+
roots = [found];
|
|
1332
|
+
}
|
|
1333
|
+
for (const root of roots) visit(root, 0);
|
|
1334
|
+
return lines.join("\n");
|
|
1335
|
+
}
|
|
1336
|
+
async function inspectDescribe(bridge, dumpOpts) {
|
|
1337
|
+
const dumpDepth = dumpOpts.maxDepth ?? 12;
|
|
1338
|
+
const result = await bridge.send({
|
|
1339
|
+
type: "query",
|
|
1340
|
+
query: { kind: "tree", depth: dumpDepth }
|
|
1341
|
+
});
|
|
1342
|
+
if (!isSemanticQueryResult(result)) return {};
|
|
1343
|
+
return {
|
|
1344
|
+
tree: formatSemanticTree(result.nodes, dumpOpts),
|
|
1345
|
+
nodeCount: result.total
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function scoreLayoutTextContrast(input) {
|
|
1349
|
+
const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
|
|
1350
|
+
const { normal, background } = input;
|
|
1351
|
+
if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
|
|
1352
|
+
return { ratio: null, required, reason: "capture dimensions do not match" };
|
|
1353
|
+
}
|
|
1354
|
+
const left = Math.max(0, Math.floor(input.box.x));
|
|
1355
|
+
const top = Math.max(0, Math.floor(input.box.y));
|
|
1356
|
+
const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
|
|
1357
|
+
const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
|
|
1358
|
+
if (right <= left || bottom <= top) {
|
|
1359
|
+
return { ratio: null, required, reason: "text is outside the captured frame" };
|
|
1360
|
+
}
|
|
1361
|
+
const linear = (value) => {
|
|
1362
|
+
const channel = value / 255;
|
|
1363
|
+
return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
1364
|
+
};
|
|
1365
|
+
const ratioOf = (foreground, behind) => {
|
|
1366
|
+
const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
|
|
1367
|
+
const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
|
|
1368
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
1369
|
+
};
|
|
1370
|
+
const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
|
|
1371
|
+
const area = (right - left) * (bottom - top);
|
|
1372
|
+
const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
|
|
1373
|
+
const offsetAt = (x, y) => (y * normal.width + x) * 4;
|
|
1374
|
+
const deltaAt = (offset) => Math.max(
|
|
1375
|
+
Math.abs(normal.data[offset] - background.data[offset]),
|
|
1376
|
+
Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
|
|
1377
|
+
Math.abs(normal.data[offset + 2] - background.data[offset + 2])
|
|
1378
|
+
);
|
|
1379
|
+
let strongestDelta = 0;
|
|
1380
|
+
let strongestForeground = null;
|
|
1381
|
+
let opaqueBackground = null;
|
|
1382
|
+
for (let y = top; y < bottom; y += step) {
|
|
1383
|
+
for (let x = left; x < right; x += step) {
|
|
1384
|
+
const offset = offsetAt(x, y);
|
|
1385
|
+
if (background.data[offset + 3] < 250) continue;
|
|
1386
|
+
const behind = [
|
|
1387
|
+
background.data[offset],
|
|
1388
|
+
background.data[offset + 1],
|
|
1389
|
+
background.data[offset + 2]
|
|
1390
|
+
];
|
|
1391
|
+
opaqueBackground ??= behind;
|
|
1392
|
+
const delta = deltaAt(offset);
|
|
1393
|
+
if (delta <= strongestDelta) continue;
|
|
1394
|
+
strongestDelta = delta;
|
|
1395
|
+
strongestForeground = [
|
|
1396
|
+
normal.data[offset],
|
|
1397
|
+
normal.data[offset + 1],
|
|
1398
|
+
normal.data[offset + 2]
|
|
1399
|
+
];
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
let bestRatio = Infinity;
|
|
1403
|
+
let bestForeground = null;
|
|
1404
|
+
let bestBackground = null;
|
|
1405
|
+
const consider = (foreground, behind) => {
|
|
1406
|
+
const ratio = ratioOf(foreground, behind);
|
|
1407
|
+
if (ratio >= bestRatio) return;
|
|
1408
|
+
bestRatio = ratio;
|
|
1409
|
+
bestForeground = foreground;
|
|
1410
|
+
bestBackground = behind;
|
|
1411
|
+
};
|
|
1412
|
+
if (strongestDelta >= 2 && strongestForeground) {
|
|
1413
|
+
for (let y = top; y < bottom; y += step) {
|
|
1414
|
+
for (let x = left; x < right; x += step) {
|
|
1415
|
+
const offset = offsetAt(x, y);
|
|
1416
|
+
if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
consider(strongestForeground, [
|
|
1420
|
+
background.data[offset],
|
|
1421
|
+
background.data[offset + 1],
|
|
1422
|
+
background.data[offset + 2]
|
|
1423
|
+
]);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
} else if (opaqueBackground) {
|
|
1427
|
+
consider(opaqueBackground, opaqueBackground);
|
|
1428
|
+
}
|
|
1429
|
+
if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
|
|
1430
|
+
return {
|
|
1431
|
+
ratio: null,
|
|
1432
|
+
required,
|
|
1433
|
+
reason: "could not isolate opaque rendered text pixels"
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
return {
|
|
1437
|
+
ratio: Math.round(bestRatio * 100) / 100,
|
|
1438
|
+
required,
|
|
1439
|
+
foreground: hex(bestForeground),
|
|
1440
|
+
background: hex(bestBackground)
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function layoutGetNativeEval(opts = {}) {
|
|
1444
|
+
const includeStyle = opts.styling === true;
|
|
1445
|
+
const filter = opts.filter?.trim().toLowerCase() ?? "";
|
|
1446
|
+
return `(async () => {
|
|
1447
|
+
const t = window.__sootsimTest
|
|
1448
|
+
if (!t || typeof t.listInspectable !== 'function') return []
|
|
1449
|
+
const list = await t.listInspectable({})
|
|
1450
|
+
if (!Array.isArray(list)) return []
|
|
1451
|
+
const includeStyle = ${JSON.stringify(includeStyle)}
|
|
1452
|
+
const scoreContrast = ${scoreLayoutTextContrast.toString()}
|
|
1453
|
+
// narrows by ELEMENT, not by style field: fewer rows, full detail on each.
|
|
1454
|
+
const filter = ${JSON.stringify(filter)}
|
|
1455
|
+
const matchesFilter = (n, text) => {
|
|
1456
|
+
if (!filter) return true
|
|
1457
|
+
const fields = [text, n.testID || n.testId, n.accessibilityRole || n.role, n.type]
|
|
1458
|
+
return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(filter))
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
|
|
1462
|
+
const round = (v) => Math.round(num(v))
|
|
1463
|
+
|
|
1464
|
+
// borderRadius can be authored as a string ("12px") or split per-corner;
|
|
1465
|
+
// prefer the uniform borderRadius, else the largest corner radius present.
|
|
1466
|
+
const radiusOf = (style) => {
|
|
1467
|
+
if (!style) return 0
|
|
1468
|
+
const keys = ['borderRadius','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius']
|
|
1469
|
+
let max = 0
|
|
1470
|
+
for (const k of keys) {
|
|
1471
|
+
const raw = style[k]
|
|
1472
|
+
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
|
|
1473
|
+
if (Number.isFinite(n)) max = Math.max(max, n)
|
|
1474
|
+
}
|
|
1475
|
+
return Math.round(max)
|
|
1476
|
+
}
|
|
1477
|
+
const fontSizeOf = (style) => {
|
|
1478
|
+
if (!style) return 0
|
|
1479
|
+
const raw = style.fontSize
|
|
1480
|
+
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
|
|
1481
|
+
return Number.isFinite(n) ? Math.round(n) : 0
|
|
1482
|
+
}
|
|
1483
|
+
// styling extractors \u2014 omit boring defaults so the agent only sees notable
|
|
1484
|
+
// styling (low opacity, struck text, clipping, a real border/color).
|
|
1485
|
+
const colorStr = (v) => {
|
|
1486
|
+
if (v == null) return undefined
|
|
1487
|
+
const s = String(v).split(' ').join('')
|
|
1488
|
+
return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
|
|
1489
|
+
}
|
|
1490
|
+
const opacityOf = (style) => {
|
|
1491
|
+
const o = style && style.opacity
|
|
1492
|
+
const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
|
|
1493
|
+
return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
|
|
1494
|
+
}
|
|
1495
|
+
const weightOf = (style) => {
|
|
1496
|
+
const w = style && style.fontWeight
|
|
1497
|
+
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
1498
|
+
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
1499
|
+
}
|
|
1500
|
+
const decoOf = (style) => {
|
|
1501
|
+
const d = style && (style.textDecorationLine || style.textDecoration)
|
|
1502
|
+
return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
|
|
1503
|
+
}
|
|
1504
|
+
const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
|
|
1505
|
+
const borderWOf = (style) => {
|
|
1506
|
+
if (!style) return undefined
|
|
1507
|
+
const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
|
|
1508
|
+
let max = 0
|
|
1509
|
+
for (const k of keys) {
|
|
1510
|
+
const r = style[k]
|
|
1511
|
+
const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
|
|
1512
|
+
if (Number.isFinite(n)) max = Math.max(max, n)
|
|
1513
|
+
}
|
|
1514
|
+
return max > 0 ? Math.round(max) : undefined
|
|
1515
|
+
}
|
|
1516
|
+
// only meaningful alongside a real border width, so it is resolved by the
|
|
1517
|
+
// caller below rather than on its own.
|
|
1518
|
+
const borderColorOf = (style) => {
|
|
1519
|
+
if (!style) return undefined
|
|
1520
|
+
const keys = ['borderColor','borderTopColor','borderBottomColor','borderLeftColor','borderRightColor']
|
|
1521
|
+
for (const k of keys) {
|
|
1522
|
+
const c = colorStr(style[k])
|
|
1523
|
+
if (c) return c
|
|
1524
|
+
}
|
|
1525
|
+
return undefined
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
const isVisible = (n) => {
|
|
1529
|
+
const layout = n && n.layout
|
|
1530
|
+
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
1531
|
+
const abs = (n && (n.absolute || n.absolutePosition)) || null
|
|
1532
|
+
if (!abs) return true
|
|
1533
|
+
const device = (n && n.device) || {}
|
|
1534
|
+
const screenW = Number(device.width) || window.innerWidth || 0
|
|
1535
|
+
const screenH = Number(device.height) || window.innerHeight || 0
|
|
1536
|
+
if (!screenW || !screenH) return true
|
|
1537
|
+
return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const elements = list.filter(isVisible).filter((n) => {
|
|
1541
|
+
const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
1542
|
+
return matchesFilter(n, t)
|
|
1543
|
+
}).map((n) => {
|
|
1544
|
+
const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
|
|
1545
|
+
const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
|
|
1546
|
+
const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
1547
|
+
const base = {
|
|
1548
|
+
...(typeof n.nodeId === 'number'
|
|
1549
|
+
? {
|
|
1550
|
+
nodeId: n.nodeId,
|
|
1551
|
+
ancestorIds: Array.isArray(n.ancestors)
|
|
1552
|
+
? n.ancestors.map((ancestor) => ancestor && ancestor.nodeId).filter((id) => typeof id === 'number')
|
|
1553
|
+
: [],
|
|
1554
|
+
}
|
|
1555
|
+
: {}),
|
|
1556
|
+
testID: n.testID || n.testId || undefined,
|
|
1557
|
+
role: n.accessibilityRole || n.role || undefined,
|
|
1558
|
+
type: typeof n.type === 'string' ? n.type : 'node',
|
|
1559
|
+
text,
|
|
1560
|
+
box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
|
|
1561
|
+
}
|
|
1562
|
+
if (!includeStyle) return base
|
|
1563
|
+
// computedStyle has the resolved values; style is the curated subset \u2014
|
|
1564
|
+
// fall back across both so radius/fontSize survive either shape.
|
|
1565
|
+
const style = Object.assign({}, n.style || {}, n.computedStyle || {})
|
|
1566
|
+
const pad = (n.boxModel && n.boxModel.padding) || {}
|
|
1567
|
+
const borderWidth = borderWOf(style)
|
|
1568
|
+
return {
|
|
1569
|
+
...base,
|
|
1570
|
+
borderRadius: radiusOf(style),
|
|
1571
|
+
padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
|
|
1572
|
+
fontSize: text ? (fontSizeOf(style) || 14) : 0,
|
|
1573
|
+
color: text ? colorStr(style.color) : undefined,
|
|
1574
|
+
bg: colorStr(style.backgroundColor),
|
|
1575
|
+
opacity: opacityOf(style),
|
|
1576
|
+
fontWeight: weightOf(style),
|
|
1577
|
+
textDecoration: decoOf(style),
|
|
1578
|
+
overflow: overflowOf(style),
|
|
1579
|
+
borderWidth,
|
|
1580
|
+
borderColor: borderWidth ? borderColorOf(style) : undefined,
|
|
1581
|
+
}
|
|
1582
|
+
})
|
|
1583
|
+
const textElements = elements.filter((element) => element.text && element.fontSize > 0)
|
|
1584
|
+
if (!includeStyle || textElements.length === 0) return elements
|
|
1585
|
+
|
|
1586
|
+
const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
|
|
1587
|
+
if (typeof screenshot !== 'function') {
|
|
1588
|
+
throw new Error('styled native layout requires the rnx capture bridge')
|
|
1589
|
+
}
|
|
1590
|
+
const device = list.find((node) => node && node.device)?.device || {}
|
|
1591
|
+
const width = Math.round(Number(device.width) || window.innerWidth || 0)
|
|
1592
|
+
const height = Math.round(Number(device.height) || window.innerHeight || 0)
|
|
1593
|
+
if (!width || !height) throw new Error('native layout has no capture dimensions')
|
|
1594
|
+
const capture = async (suppressText) => {
|
|
1595
|
+
const dataUrl = await screenshot({
|
|
1596
|
+
format: 'png',
|
|
1597
|
+
outputWidth: width,
|
|
1598
|
+
outputHeight: height,
|
|
1599
|
+
...(suppressText ? { suppressText: true } : {}),
|
|
1600
|
+
})
|
|
1601
|
+
const image = await new Promise((resolve, reject) => {
|
|
1602
|
+
const next = new Image()
|
|
1603
|
+
next.onload = () => resolve(next)
|
|
1604
|
+
next.onerror = () => reject(new Error('could not decode native layout pixels'))
|
|
1605
|
+
next.src = dataUrl
|
|
1606
|
+
})
|
|
1607
|
+
const canvas = document.createElement('canvas')
|
|
1608
|
+
canvas.width = image.naturalWidth
|
|
1609
|
+
canvas.height = image.naturalHeight
|
|
1610
|
+
const context = canvas.getContext('2d', { willReadFrequently: true })
|
|
1611
|
+
if (!context) throw new Error('could not read native layout pixels')
|
|
1612
|
+
context.drawImage(image, 0, 0)
|
|
1613
|
+
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
1614
|
+
}
|
|
1615
|
+
const normal = await capture(false)
|
|
1616
|
+
const background = await capture(true)
|
|
1617
|
+
for (const element of textElements) {
|
|
1618
|
+
element.contrast = scoreContrast({
|
|
1619
|
+
normal,
|
|
1620
|
+
background,
|
|
1621
|
+
box: element.box,
|
|
1622
|
+
fontSize: element.fontSize,
|
|
1623
|
+
fontWeight: element.fontWeight,
|
|
1624
|
+
})
|
|
1625
|
+
}
|
|
1626
|
+
return elements
|
|
1627
|
+
})()`;
|
|
1628
|
+
}
|
|
1629
|
+
var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
|
|
1630
|
+
async function inspectAccessibilityTree(bridge) {
|
|
1631
|
+
const result = await bridge.send({
|
|
1632
|
+
type: "query",
|
|
1633
|
+
query: { kind: "find", selector: { visible: true } }
|
|
1634
|
+
});
|
|
1635
|
+
if (!isSemanticQueryResult(result)) return [];
|
|
1636
|
+
return result.nodes.flatMap((node) => {
|
|
1637
|
+
if (!node.role && !node.label && !node.text && !node.pressable) return [];
|
|
1638
|
+
return [
|
|
1639
|
+
{
|
|
1640
|
+
role: node.role ?? (node.pressable ? "button" : node.type === "text" ? "statictext" : "none"),
|
|
1641
|
+
label: node.label ?? node.text ?? null,
|
|
1642
|
+
hint: null,
|
|
1643
|
+
state: null,
|
|
1644
|
+
testID: node.testID ?? null,
|
|
1645
|
+
position: {
|
|
1646
|
+
x: Math.round(node.geometry.x),
|
|
1647
|
+
y: Math.round(node.geometry.y)
|
|
1648
|
+
},
|
|
1649
|
+
size: {
|
|
1650
|
+
w: Math.round(node.geometry.width),
|
|
1651
|
+
h: Math.round(node.geometry.height)
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
];
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
function resolveFindMode(q) {
|
|
1658
|
+
if (q.testId) {
|
|
1659
|
+
return { mode: "testid", selector: { testID: q.testId } };
|
|
1660
|
+
}
|
|
1661
|
+
if (q.role) {
|
|
1662
|
+
return { mode: "role", selector: { role: q.role } };
|
|
1663
|
+
}
|
|
1664
|
+
if (q.type) {
|
|
1665
|
+
return { mode: "type", selector: { type: q.type } };
|
|
1666
|
+
}
|
|
1667
|
+
if (q.pressable) {
|
|
1668
|
+
return { mode: "pressable", selector: { pressable: true } };
|
|
1669
|
+
}
|
|
1670
|
+
if (q.interactive) {
|
|
1671
|
+
return { mode: "interactive-targets", selector: { pressable: true } };
|
|
1672
|
+
}
|
|
1673
|
+
if (q.visible) {
|
|
1674
|
+
return { mode: "visible", selector: { visible: true } };
|
|
1675
|
+
}
|
|
1676
|
+
if (q.text) {
|
|
1677
|
+
return { mode: "text", selector: { text: q.text } };
|
|
1678
|
+
}
|
|
1679
|
+
return null;
|
|
1680
|
+
}
|
|
1681
|
+
async function inspectFind(bridge, q) {
|
|
1682
|
+
const resolved = resolveFindMode(q);
|
|
1683
|
+
if (!resolved) return null;
|
|
1684
|
+
const result = await bridge.send({
|
|
1685
|
+
type: "query",
|
|
1686
|
+
query: { kind: "find", selector: resolved.selector }
|
|
1687
|
+
});
|
|
1688
|
+
if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] };
|
|
1689
|
+
const nodes = result.nodes.map(
|
|
1690
|
+
(node) => ({
|
|
1691
|
+
type: node.type,
|
|
1692
|
+
nodeId: node.nodeId,
|
|
1693
|
+
...node.testID ? { testID: node.testID } : {},
|
|
1694
|
+
...node.text ? { text: node.text } : {},
|
|
1695
|
+
...node.pressable ? { pressable: true } : {},
|
|
1696
|
+
...node.role ? { accessibilityRole: node.role } : {},
|
|
1697
|
+
...node.label ? { accessibilityLabel: node.label } : {},
|
|
1698
|
+
absolutePosition: { x: node.geometry.x, y: node.geometry.y },
|
|
1699
|
+
layout: { width: node.geometry.width, height: node.geometry.height }
|
|
1700
|
+
})
|
|
1701
|
+
);
|
|
1702
|
+
const resultValue = resolved.mode === "testid" || resolved.mode === "text" ? nodes[0] ?? null : nodes;
|
|
1703
|
+
return { mode: resolved.mode, result: resultValue };
|
|
1704
|
+
}
|
|
1705
|
+
function waitReadyReason(status) {
|
|
1706
|
+
if (status.bridgeError) {
|
|
1707
|
+
if (status.bridgeError.includes(HEADLESS_TENANT_FETCH_HUNG_MARKER) || status.bridgeError.includes(PUBLIC_NO_NETWORK_MARKER)) {
|
|
1708
|
+
return status.bridgeError;
|
|
1709
|
+
}
|
|
1710
|
+
return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
|
|
1711
|
+
}
|
|
1712
|
+
const waitingOnGuestFetch = status.externalReady === false && /https?:\/\//.test(status.externalStatus);
|
|
1713
|
+
const base = status.externalError ? `guest app errored: ${status.externalError}` : waitingOnGuestFetch ? `waiting on a guest fetch: ${status.externalStatus}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
|
|
1714
|
+
return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
|
|
1715
|
+
}
|
|
1716
|
+
async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
|
|
1717
|
+
const start = Date.now();
|
|
1718
|
+
let result;
|
|
1719
|
+
try {
|
|
1720
|
+
result = await bridge.send(
|
|
1721
|
+
{
|
|
1722
|
+
type: "waitFor",
|
|
1723
|
+
...options.simId ? { simId: options.simId } : {},
|
|
1724
|
+
waitForOptions: { condition: { type: "ready" }, timeoutMs }
|
|
1725
|
+
},
|
|
1726
|
+
{ timeoutMs: timeoutMs + 1e3 }
|
|
1727
|
+
);
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
const bridgeError = error instanceof Error ? error.message : String(error);
|
|
1730
|
+
if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
|
|
1731
|
+
throw error;
|
|
1732
|
+
}
|
|
1733
|
+
result = {
|
|
1734
|
+
matched: false,
|
|
1735
|
+
elapsedMs: Date.now() - start,
|
|
1736
|
+
polls: 0,
|
|
1737
|
+
error: bridgeError
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
const probe = result.probe;
|
|
1741
|
+
const externalError = probe?.externalError ?? "";
|
|
1742
|
+
const status = {
|
|
1743
|
+
ready: result.matched && !externalError,
|
|
1744
|
+
elapsedMs: result.elapsedMs,
|
|
1745
|
+
nodes: probe?.nodes ?? 0,
|
|
1746
|
+
targets: probe?.targets ?? 0,
|
|
1747
|
+
liveFrameActive: probe?.liveFrameActive ?? false,
|
|
1748
|
+
liveFrameChannels: probe?.liveFrameChannels ?? 0,
|
|
1749
|
+
liveFramePublishes: probe?.liveFramePublishes ?? 0,
|
|
1750
|
+
flag: probe?.flag,
|
|
1751
|
+
loadingText: probe?.loadingText ?? "",
|
|
1752
|
+
externalReady: probe?.externalReady ?? null,
|
|
1753
|
+
externalStatus: probe?.externalStatus ?? "",
|
|
1754
|
+
externalError,
|
|
1755
|
+
suppressedEntryError: probe?.suppressedEntryError ?? "",
|
|
1756
|
+
errors: probe?.errors ?? 0,
|
|
1757
|
+
bridgeError: result.error ?? ""
|
|
1758
|
+
};
|
|
1759
|
+
if (!status.ready && options.onProgress) options.onProgress(status);
|
|
1760
|
+
return status;
|
|
1761
|
+
}
|
|
1762
|
+
function consoleEntriesEval(level, limit) {
|
|
1763
|
+
const getter = level === "error" ? "getErrors" : "getWarnings";
|
|
1764
|
+
return `(() => {
|
|
1765
|
+
const out = []
|
|
1766
|
+
const seen = new Set()
|
|
1767
|
+
const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
|
|
1768
|
+
const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
|
|
1769
|
+
const push = (e) => {
|
|
1770
|
+
if (!e) return
|
|
1771
|
+
const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
|
|
1772
|
+
const k = key(ts, e.args)
|
|
1773
|
+
if (seen.has(k)) return
|
|
1774
|
+
seen.add(k)
|
|
1775
|
+
out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
|
|
1776
|
+
}
|
|
1777
|
+
// page-realm ws-bridge buffer first (richest stacks for page-origin errors)
|
|
1778
|
+
try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
|
|
1779
|
+
// engine observability store \u2014 the always-on render-worker / forwarded path
|
|
1780
|
+
try {
|
|
1781
|
+
const obs = window.__sootsimObservability
|
|
1782
|
+
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
1783
|
+
for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
|
|
1784
|
+
} catch {}
|
|
1785
|
+
return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
|
|
1786
|
+
})()`;
|
|
1787
|
+
}
|
|
1788
|
+
async function inspectErrors(bridge, limit = 20) {
|
|
1789
|
+
const result = await bridge.send({
|
|
1790
|
+
type: "evaluate",
|
|
1791
|
+
code: consoleEntriesEval("error", limit)
|
|
1792
|
+
});
|
|
1793
|
+
return Array.isArray(result) ? result : [];
|
|
1794
|
+
}
|
|
1795
|
+
async function inspectWarnings(bridge, limit = 20) {
|
|
1796
|
+
const result = await bridge.send({
|
|
1797
|
+
type: "evaluate",
|
|
1798
|
+
code: consoleEntriesEval("warn", limit)
|
|
1799
|
+
});
|
|
1800
|
+
return Array.isArray(result) ? result : [];
|
|
1801
|
+
}
|
|
1802
|
+
async function inspectLogs(bridge) {
|
|
1803
|
+
if (bridge.plane === "cloud") {
|
|
1804
|
+
const state = await bridge.send({
|
|
1805
|
+
type: "state",
|
|
1806
|
+
stateOptions: {
|
|
1807
|
+
screenshot: false,
|
|
1808
|
+
tree: false,
|
|
1809
|
+
route: false,
|
|
1810
|
+
errors: false,
|
|
1811
|
+
logs: true,
|
|
1812
|
+
logLimit: 100
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
const recentLogs = state && typeof state === "object" && Array.isArray(Reflect.get(state, "recentLogs")) ? Reflect.get(state, "recentLogs") : [];
|
|
1816
|
+
return recentLogs.flatMap((entry, index) => {
|
|
1817
|
+
if (!entry || typeof entry !== "object") return [];
|
|
1818
|
+
const text = Reflect.get(entry, "text");
|
|
1819
|
+
const rawLevel = Reflect.get(entry, "level");
|
|
1820
|
+
if (typeof text !== "string") return [];
|
|
1821
|
+
const level = rawLevel === "log" || rawLevel === "info" || rawLevel === "warn" || rawLevel === "error" || rawLevel === "debug" ? rawLevel : "error";
|
|
1822
|
+
const at = Reflect.get(entry, "at");
|
|
1823
|
+
const ts = typeof at === "number" && Number.isFinite(at) ? at : 0;
|
|
1824
|
+
return [
|
|
1825
|
+
{
|
|
1826
|
+
id: `cloud-${ts}-${index}`,
|
|
1827
|
+
source: "rnx-cloud",
|
|
1828
|
+
level,
|
|
1829
|
+
ts,
|
|
1830
|
+
args: [text],
|
|
1831
|
+
stack: null
|
|
1832
|
+
}
|
|
1833
|
+
];
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
const res = await bridge.send({
|
|
1837
|
+
type: "evaluate",
|
|
1838
|
+
code: `(() => {
|
|
1839
|
+
const obs = window.__sootsimObservability;
|
|
1840
|
+
if (!obs) return { ok: false };
|
|
1841
|
+
return { ok: true, entries: obs.logs.getSnapshot() };
|
|
1842
|
+
})()`
|
|
1843
|
+
});
|
|
1844
|
+
if (!res || !res.ok) return [];
|
|
1845
|
+
return res.entries ?? [];
|
|
1846
|
+
}
|
|
1847
|
+
function isForwardedWorkerLogTwin(a, b) {
|
|
1848
|
+
if (a.source === b.source) return false;
|
|
1849
|
+
if (a.level !== b.level) return false;
|
|
1850
|
+
if (Math.abs(a.ts - b.ts) > 1e3) return false;
|
|
1851
|
+
if (a.args.length !== b.args.length) return false;
|
|
1852
|
+
const sources = /* @__PURE__ */ new Set([a.source, b.source]);
|
|
1853
|
+
if (!sources.has("sootsim-worker")) return false;
|
|
1854
|
+
if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
return a.args.every((arg, index) => arg === b.args[index]);
|
|
1858
|
+
}
|
|
1859
|
+
function filterLogEntries(entries, opts = {}) {
|
|
1860
|
+
let out = [];
|
|
1861
|
+
for (const entry of entries) {
|
|
1862
|
+
if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
out.push(entry);
|
|
1866
|
+
}
|
|
1867
|
+
if (!opts.showInternal) {
|
|
1868
|
+
out = out.filter((e) => {
|
|
1869
|
+
const first = e.args[0];
|
|
1870
|
+
return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
if (opts.level) out = out.filter((e) => opts.level.has(e.level));
|
|
1874
|
+
if (opts.filter) {
|
|
1875
|
+
const lf = opts.filter.toLowerCase();
|
|
1876
|
+
out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
|
|
1877
|
+
}
|
|
1878
|
+
return out;
|
|
1879
|
+
}
|
|
1880
|
+
async function inspectKeyboard(bridge) {
|
|
1881
|
+
const result = await bridge.send({
|
|
1882
|
+
type: "evaluate",
|
|
1883
|
+
code: `(() => {
|
|
1884
|
+
const kb = window.__sootsimKeyboard
|
|
1885
|
+
if (!kb || typeof kb.getLayout !== 'function') {
|
|
1886
|
+
return { error: 'keyboard bridge getLayout() not available' }
|
|
1887
|
+
}
|
|
1888
|
+
return kb.getLayout()
|
|
1889
|
+
})()`
|
|
1890
|
+
});
|
|
1891
|
+
return result ?? { error: "keyboard bridge returned no result" };
|
|
1892
|
+
}
|
|
1893
|
+
function isShellCommandUnavailable(error) {
|
|
1894
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1895
|
+
return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
|
|
1896
|
+
}
|
|
1897
|
+
async function getShellState(bridge, readyTimeoutMs = 0) {
|
|
1898
|
+
const deadline = Date.now() + Math.max(0, readyTimeoutMs);
|
|
1899
|
+
while (true) {
|
|
1900
|
+
try {
|
|
1901
|
+
return await bridge.send({
|
|
1902
|
+
type: "call",
|
|
1903
|
+
path: "SootSim.bridges.mainShell.getState",
|
|
1904
|
+
args: []
|
|
1905
|
+
});
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
|
|
1908
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// cli/commands/inspect/resolve-target.ts
|
|
1914
|
+
async function resolveTargetCoords(bridge, target) {
|
|
1915
|
+
const result = await bridge.send({
|
|
1916
|
+
type: "resolve",
|
|
1917
|
+
selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
|
|
1918
|
+
});
|
|
1919
|
+
if (result === null) return null;
|
|
1920
|
+
if (typeof result !== "object") throw new Error("resolve returned an invalid result");
|
|
1921
|
+
const match = Reflect.get(result, "match");
|
|
1922
|
+
const resolvedTarget = Reflect.get(result, "target");
|
|
1923
|
+
const semanticNodes = [match, resolvedTarget];
|
|
1924
|
+
const point = Reflect.get(result, "point");
|
|
1925
|
+
const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
|
|
1926
|
+
const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
|
|
1927
|
+
if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
|
|
1928
|
+
throw new Error("resolve returned an invalid result");
|
|
1929
|
+
}
|
|
1930
|
+
const semanticMatch = semanticNodes[1];
|
|
1931
|
+
if (!semanticMatch) throw new Error("resolve returned an invalid result");
|
|
1932
|
+
return {
|
|
1933
|
+
x,
|
|
1934
|
+
y,
|
|
1935
|
+
nodeId: semanticMatch.nodeId,
|
|
1936
|
+
testID: semanticMatch.testID ?? null,
|
|
1937
|
+
text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
|
|
1938
|
+
type: semanticMatch.type
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// cli/commands/inspect/settling.ts
|
|
1943
|
+
async function waitForSootsimIdle({
|
|
1944
|
+
bridge,
|
|
1945
|
+
simId,
|
|
1946
|
+
maxMs,
|
|
1947
|
+
pollMs = 50,
|
|
1948
|
+
stablePolls = 3,
|
|
1949
|
+
strict = false
|
|
1950
|
+
}) {
|
|
1951
|
+
const result = await bridge.send(
|
|
1952
|
+
{
|
|
1953
|
+
type: "evaluate",
|
|
1954
|
+
simId,
|
|
1955
|
+
code: `(async () => {
|
|
1956
|
+
const start = Date.now()
|
|
1957
|
+
const deadline = start + ${Math.max(0, Math.round(maxMs))}
|
|
1958
|
+
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
1959
|
+
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
1960
|
+
const strict = ${strict ? "true" : "false"}
|
|
1961
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1962
|
+
|
|
1963
|
+
// route-aware settle: a tap that pushes/pops a screen is async \u2014 the
|
|
1964
|
+
// old screen still renders for a moment, then the new one mounts and
|
|
1965
|
+
// its content loads. a pure layout-hash check reports "idle" on the
|
|
1966
|
+
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
1967
|
+
// and stacks duplicate navigations. drain any in-flight screen
|
|
1968
|
+
// transition FIRST, bounded by the overall budget. when no
|
|
1969
|
+
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
1970
|
+
// a plain button tap barely pays for it.
|
|
1971
|
+
try {
|
|
1972
|
+
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
1973
|
+
if (typeof wfst === 'function') {
|
|
1974
|
+
const remaining = deadline - Date.now()
|
|
1975
|
+
if (remaining > 120) {
|
|
1976
|
+
await wfst({
|
|
1977
|
+
timeoutMs: Math.min(remaining - 80, 4000),
|
|
1978
|
+
settleMs: 64,
|
|
1979
|
+
startWindowMs: 80,
|
|
1980
|
+
})
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
} catch {}
|
|
1984
|
+
|
|
1985
|
+
const readSnapshot = async () => {
|
|
1986
|
+
let animating = false
|
|
1987
|
+
let layoutDirty = false
|
|
1988
|
+
let pendingFetches = 0
|
|
1989
|
+
let requestTotal = 0
|
|
1990
|
+
let requestInFlight = 0
|
|
1991
|
+
let renderStatsAvailable = false
|
|
1992
|
+
try {
|
|
1993
|
+
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
1994
|
+
if (stats) {
|
|
1995
|
+
renderStatsAvailable = true
|
|
1996
|
+
animating =
|
|
1997
|
+
stats.hasActiveAnims === true ||
|
|
1998
|
+
stats.hasActiveNativeAnimations === true ||
|
|
1999
|
+
stats.hasPendingAnimationFrames === true
|
|
2000
|
+
layoutDirty =
|
|
2001
|
+
stats.layoutDirty === true && stats.renderRequested === true
|
|
2002
|
+
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
2003
|
+
// but not actually settled \u2014 its data/images are still in
|
|
2004
|
+
// flight. treat bounded image-loader fetches as not-idle so
|
|
2005
|
+
// settle waits for real content, capped by the budget.
|
|
2006
|
+
const pf = stats.memory && stats.memory.imageLoader
|
|
2007
|
+
? stats.memory.imageLoader.pendingFetches
|
|
2008
|
+
: 0
|
|
2009
|
+
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
2010
|
+
}
|
|
2011
|
+
} catch {}
|
|
2012
|
+
try {
|
|
2013
|
+
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
2014
|
+
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
2015
|
+
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
2016
|
+
} catch {}
|
|
2017
|
+
const root = window.__sootsimRoot
|
|
2018
|
+
const nodes = []
|
|
2019
|
+
if (root) {
|
|
2020
|
+
const walk = (n) => {
|
|
2021
|
+
if (n.layout && n.layout.width > 0) {
|
|
2022
|
+
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
2023
|
+
}
|
|
2024
|
+
for (const c of n.children || []) walk(c)
|
|
2025
|
+
}
|
|
2026
|
+
walk(root)
|
|
2027
|
+
}
|
|
2028
|
+
return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// how long generic background network is allowed to keep us waiting AFTER
|
|
2032
|
+
// the screen is otherwise visually settled. apps with continuous traffic
|
|
2033
|
+
// (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
|
|
2034
|
+
// requestInFlight===0 / a stable requestTotal, so requiring full network
|
|
2035
|
+
// quiet burns the ENTIRE budget on every command \u2014 the leading idle that
|
|
2036
|
+
// failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
|
|
2037
|
+
// layout-stability + the image-loader already prove visible content
|
|
2038
|
+
// loaded (a data load changes layout; an image load shows in the loader),
|
|
2039
|
+
// so the generic request counters are a courtesy, not a gate: honor them
|
|
2040
|
+
// only within this grace once the screen is visually still.
|
|
2041
|
+
const networkQuietGraceMs = 1200
|
|
2042
|
+
let lastLayout = ''
|
|
2043
|
+
let lastRequestTotal = -1
|
|
2044
|
+
let stable = 0
|
|
2045
|
+
let visuallyStillSince = 0
|
|
2046
|
+
// counts across the whole budget: a flag that is true on every poll is a
|
|
2047
|
+
// different bug from one that is true intermittently, and the two need
|
|
2048
|
+
// opposite fixes (engine vs budget).
|
|
2049
|
+
let polls = 0
|
|
2050
|
+
let dirtyPolls = 0
|
|
2051
|
+
let animatingPolls = 0
|
|
2052
|
+
let layoutChangedPolls = 0
|
|
2053
|
+
while (Date.now() < deadline) {
|
|
2054
|
+
const snapshot = await readSnapshot()
|
|
2055
|
+
polls++
|
|
2056
|
+
if (snapshot.layoutDirty) dirtyPolls++
|
|
2057
|
+
if (snapshot.animating) animatingPolls++
|
|
2058
|
+
if (polls > 1 && snapshot.layout !== lastLayout) layoutChangedPolls++
|
|
2059
|
+
const strictOk =
|
|
2060
|
+
!strict ||
|
|
2061
|
+
(snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
|
|
2062
|
+
// visually settled: no animations, no image content loading, layout
|
|
2063
|
+
// unchanged since the last poll.
|
|
2064
|
+
const visuallyStill =
|
|
2065
|
+
strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
|
|
2066
|
+
// generic network quiet: nothing in flight and no new requests issued.
|
|
2067
|
+
const networkQuiet =
|
|
2068
|
+
snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
|
|
2069
|
+
if (visuallyStill) {
|
|
2070
|
+
stable++
|
|
2071
|
+
if (visuallyStillSince === 0) visuallyStillSince = Date.now()
|
|
2072
|
+
// settle once the screen has held still long enough AND either the
|
|
2073
|
+
// network is genuinely quiet OR it has refused to quiet within the
|
|
2074
|
+
// grace (continuous background traffic must not block forever).
|
|
2075
|
+
if (
|
|
2076
|
+
stable >= requiredStablePolls &&
|
|
2077
|
+
(networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
|
|
2078
|
+
) {
|
|
2079
|
+
return { settled: true, elapsed: Date.now() - start, blockedBy: '' }
|
|
2080
|
+
}
|
|
2081
|
+
} else {
|
|
2082
|
+
stable = 0
|
|
2083
|
+
visuallyStillSince = 0
|
|
2084
|
+
}
|
|
2085
|
+
lastLayout = snapshot.layout
|
|
2086
|
+
lastRequestTotal = snapshot.requestTotal
|
|
2087
|
+
await sleep(pollMs)
|
|
2088
|
+
}
|
|
2089
|
+
const last = await readSnapshot()
|
|
2090
|
+
const blockers = []
|
|
2091
|
+
if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
|
|
2092
|
+
if (last.animating) blockers.push('animations still running')
|
|
2093
|
+
if (last.layoutDirty) blockers.push('layout still dirty')
|
|
2094
|
+
if (last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
2095
|
+
if (last.layout !== lastLayout) blockers.push('layout still changing')
|
|
2096
|
+
if (last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
2097
|
+
return {
|
|
2098
|
+
settled: false,
|
|
2099
|
+
elapsed: Date.now() - start,
|
|
2100
|
+
blockedBy:
|
|
2101
|
+
(blockers.length ? blockers.join(', ') : 'unknown') +
|
|
2102
|
+
' over ' + polls + ' polls (layout changed ' + layoutChangedPolls +
|
|
2103
|
+
'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x)',
|
|
2104
|
+
}
|
|
2105
|
+
})()`
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
timeoutMs: maxMs + 1e3
|
|
2109
|
+
}
|
|
2110
|
+
);
|
|
2111
|
+
const { elapsed, settled, blockedBy } = result ?? {};
|
|
2112
|
+
return {
|
|
2113
|
+
elapsed: typeof elapsed === "number" ? elapsed : maxMs,
|
|
2114
|
+
settled: settled === true,
|
|
2115
|
+
blockedBy: typeof blockedBy === "string" ? blockedBy : "unknown"
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// cli/commands/inspect/actions.ts
|
|
2120
|
+
function isScreenSize(value) {
|
|
2121
|
+
if (value === null || typeof value !== "object") return false;
|
|
2122
|
+
const width = Reflect.get(value, "width");
|
|
2123
|
+
const height = Reflect.get(value, "height");
|
|
2124
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
|
|
2125
|
+
}
|
|
2126
|
+
function viewportFromCapture(value) {
|
|
2127
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
2128
|
+
const tree = Reflect.get(value, "tree");
|
|
2129
|
+
return tree !== null && typeof tree === "object" ? Reflect.get(tree, "viewport") : void 0;
|
|
2130
|
+
}
|
|
2131
|
+
async function readScreen(bridge) {
|
|
2132
|
+
try {
|
|
2133
|
+
const viewport = viewportFromCapture(
|
|
2134
|
+
await bridge.send({ type: "capture", includeVisual: false })
|
|
2135
|
+
);
|
|
2136
|
+
return isScreenSize(viewport) ? { width: viewport.width, height: viewport.height } : null;
|
|
2137
|
+
} catch {
|
|
2138
|
+
return null;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
function isOffscreen(cx, cy, screen) {
|
|
2142
|
+
return !(cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height);
|
|
2143
|
+
}
|
|
2144
|
+
var DEFAULT_AGENT_TIMING = {
|
|
2145
|
+
initialWaitMs: 3e3,
|
|
2146
|
+
deadlineMs: 5e3,
|
|
2147
|
+
retryWaitMs: 700
|
|
2148
|
+
};
|
|
2149
|
+
var DEFAULT_INTERACTIVE_TIMING = {
|
|
2150
|
+
initialWaitMs: 1200,
|
|
2151
|
+
deadlineMs: 2500,
|
|
2152
|
+
retryWaitMs: 700
|
|
2153
|
+
};
|
|
2154
|
+
function tapTiming(agent, opts = {}) {
|
|
2155
|
+
return {
|
|
2156
|
+
...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
|
|
2157
|
+
...opts
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function isTapSuccess(result) {
|
|
2161
|
+
if (!result || result.hit === false || result.ok === false) return false;
|
|
2162
|
+
if (result.handled === false) return false;
|
|
2163
|
+
if (result.requestedTargetMatched === false) return false;
|
|
2164
|
+
if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
function tapTargetFromPayload(payload, textFallback) {
|
|
2170
|
+
const id = payload.target?.id ?? payload.match?.id ?? payload.node?.id;
|
|
2171
|
+
return {
|
|
2172
|
+
nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
|
|
2173
|
+
id: typeof id === "string" ? id : null,
|
|
2174
|
+
testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
|
|
2175
|
+
text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
|
|
2176
|
+
type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
async function tapResolvedTarget(bridge, args) {
|
|
2180
|
+
const timing = tapTiming(!!args.agent, args.timing);
|
|
2181
|
+
let attempts = 0;
|
|
2182
|
+
let lastPayload = null;
|
|
2183
|
+
let lastResult = null;
|
|
2184
|
+
const settleCloud = async () => {
|
|
2185
|
+
try {
|
|
2186
|
+
await bridge.send({ type: "settle" });
|
|
2187
|
+
} catch {
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
if (bridge.plane === "cloud") {
|
|
2191
|
+
await settleCloud();
|
|
2192
|
+
} else if (timing.initialWaitMs > 0) {
|
|
2193
|
+
try {
|
|
2194
|
+
await waitForSootsimIdle({
|
|
2195
|
+
bridge,
|
|
2196
|
+
maxMs: timing.initialWaitMs,
|
|
2197
|
+
pollMs: 32,
|
|
2198
|
+
stablePolls: 2
|
|
2199
|
+
});
|
|
2200
|
+
} catch {
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
const deadline = Date.now() + timing.deadlineMs;
|
|
2204
|
+
let lastOffscreenAt = null;
|
|
2205
|
+
const screen = bridge.plane === "cloud" ? null : await readScreen(bridge);
|
|
2206
|
+
while (Date.now() <= deadline || attempts === 0) {
|
|
2207
|
+
attempts++;
|
|
2208
|
+
const resolved = await args.resolve();
|
|
2209
|
+
const payload = resolved && typeof resolved.cx === "number" && typeof resolved.cy === "number" && screen && isOffscreen(resolved.cx, resolved.cy, screen) ? { ...resolved, offscreen: true, screen } : resolved;
|
|
2210
|
+
lastPayload = payload;
|
|
2211
|
+
if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
|
|
2212
|
+
return { payload, result: null, attempts, failure: "special" };
|
|
2213
|
+
}
|
|
2214
|
+
if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
|
|
2215
|
+
if (payload.offscreen) {
|
|
2216
|
+
lastResult = {
|
|
2217
|
+
hit: false,
|
|
2218
|
+
reason: "offscreen",
|
|
2219
|
+
x: payload.cx,
|
|
2220
|
+
y: payload.cy,
|
|
2221
|
+
screen: payload.screen
|
|
2222
|
+
};
|
|
2223
|
+
const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
|
|
2224
|
+
if (lastOffscreenAt === at) {
|
|
2225
|
+
return { payload, result: lastResult, attempts, failure: "missed" };
|
|
2226
|
+
}
|
|
2227
|
+
lastOffscreenAt = at;
|
|
2228
|
+
} else {
|
|
2229
|
+
lastOffscreenAt = null;
|
|
2230
|
+
const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
|
|
2231
|
+
const result = await tapCoordinates(
|
|
2232
|
+
bridge,
|
|
2233
|
+
payload.cx,
|
|
2234
|
+
payload.cy,
|
|
2235
|
+
requestedTarget
|
|
2236
|
+
);
|
|
2237
|
+
lastResult = result?.reason === "offscreen" && result.diagnostics ? {
|
|
2238
|
+
...result,
|
|
2239
|
+
hit: false,
|
|
2240
|
+
x: result.diagnostics.point?.x ?? payload.cx,
|
|
2241
|
+
y: result.diagnostics.point?.y ?? payload.cy,
|
|
2242
|
+
screen: result.diagnostics.screen
|
|
2243
|
+
} : result;
|
|
2244
|
+
if (result?.requestedTargetMatched === false) {
|
|
2245
|
+
return { payload, result, attempts, failure: "missed" };
|
|
2246
|
+
}
|
|
2247
|
+
if (isTapSuccess(result)) return { payload, result, attempts };
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
const remaining = deadline - Date.now();
|
|
2251
|
+
if (remaining <= 0) break;
|
|
2252
|
+
if (bridge.plane === "cloud") {
|
|
2253
|
+
if (lastResult?.reason !== "target-moved" || attempts >= 3) break;
|
|
2254
|
+
await settleCloud();
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
if (timing.retryWaitMs <= 0) break;
|
|
2258
|
+
try {
|
|
2259
|
+
await waitForSootsimIdle({
|
|
2260
|
+
bridge,
|
|
2261
|
+
maxMs: Math.min(remaining, timing.retryWaitMs),
|
|
2262
|
+
pollMs: 32,
|
|
2263
|
+
stablePolls: 2
|
|
2264
|
+
});
|
|
2265
|
+
} catch {
|
|
2266
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.min(120, remaining)));
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return {
|
|
2270
|
+
payload: lastPayload,
|
|
2271
|
+
result: lastResult,
|
|
2272
|
+
attempts,
|
|
2273
|
+
failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
async function tapCoordinates(bridge, x, y, target) {
|
|
2277
|
+
return bridge.send({
|
|
2278
|
+
type: "tap",
|
|
2279
|
+
x,
|
|
2280
|
+
y,
|
|
2281
|
+
...target ? { target } : {}
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
function payloadFromResolved(resolved, match, extra = {}) {
|
|
2285
|
+
return {
|
|
2286
|
+
cx: resolved.x,
|
|
2287
|
+
cy: resolved.y,
|
|
2288
|
+
match: {
|
|
2289
|
+
nodeId: match.nodeId ?? null,
|
|
2290
|
+
id: match.testID ?? null,
|
|
2291
|
+
testID: match.testID ?? null,
|
|
2292
|
+
text: match.text ?? null,
|
|
2293
|
+
type: match.type ?? null
|
|
2294
|
+
},
|
|
2295
|
+
target: {
|
|
2296
|
+
nodeId: resolved.nodeId ?? match.nodeId ?? null,
|
|
2297
|
+
id: resolved.testID ?? match.testID ?? null,
|
|
2298
|
+
testID: resolved.testID ?? match.testID ?? null,
|
|
2299
|
+
text: resolved.text ?? match.text ?? null,
|
|
2300
|
+
type: resolved.type ?? match.type ?? null
|
|
2301
|
+
},
|
|
2302
|
+
strategy: "matched-node",
|
|
2303
|
+
...extra
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "") {
|
|
2307
|
+
const out = [];
|
|
2308
|
+
for (const node of nodes) {
|
|
2309
|
+
const text = nodeText(node);
|
|
2310
|
+
const caption = !!text && text === pressableOwnerText && !node.pressable;
|
|
2311
|
+
if (!caption) out.push({ node, ancestorTestIDs: ancestors });
|
|
2312
|
+
if (node.children?.length) {
|
|
2313
|
+
const next = node.testID ? [...ancestors, node.testID] : ancestors;
|
|
2314
|
+
const owner = text ? node.pressable ? text : "" : pressableOwnerText;
|
|
2315
|
+
out.push(...collectTextCandidates(node.children, next, owner));
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return out;
|
|
2319
|
+
}
|
|
2320
|
+
function nodeText(node) {
|
|
2321
|
+
return node.text ?? node.label ?? "";
|
|
2322
|
+
}
|
|
2323
|
+
async function tapById(bridge, query, opts = {}) {
|
|
2324
|
+
return tapResolvedTarget(bridge, {
|
|
2325
|
+
agent: opts.agent,
|
|
2326
|
+
timing: opts.timing,
|
|
2327
|
+
resolve: async () => {
|
|
2328
|
+
const resolved = await resolveTargetCoords(bridge, { mode: "testid", value: query });
|
|
2329
|
+
if (!resolved) return { cx: null };
|
|
2330
|
+
return payloadFromResolved(resolved, resolved);
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
async function tapByText(bridge, query, options = {}, opts = {}) {
|
|
2335
|
+
return tapResolvedTarget(bridge, {
|
|
2336
|
+
agent: opts.agent,
|
|
2337
|
+
timing: opts.timing,
|
|
2338
|
+
textFallback: query,
|
|
2339
|
+
resolve: async () => {
|
|
2340
|
+
const { tree } = await inspectTree(bridge, 50);
|
|
2341
|
+
if (!Array.isArray(tree)) return { matched: 0, total: 0 };
|
|
2342
|
+
const needle = options.exact ? query : query.toLowerCase();
|
|
2343
|
+
let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
|
|
2344
|
+
const text = nodeText(node2);
|
|
2345
|
+
if (!text) return false;
|
|
2346
|
+
if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
if (options.role && node2.role !== options.role) return false;
|
|
2350
|
+
if (options.within && !ancestorTestIDs.includes(options.within)) return false;
|
|
2351
|
+
const ax = node2.geometry.x;
|
|
2352
|
+
const ay = node2.geometry.y;
|
|
2353
|
+
if (options.minX != null && !(ax >= options.minX)) return false;
|
|
2354
|
+
if (options.maxX != null && !(ax <= options.maxX)) return false;
|
|
2355
|
+
if (options.minY != null && !(ay >= options.minY)) return false;
|
|
2356
|
+
if (options.maxY != null && !(ay <= options.maxY)) return false;
|
|
2357
|
+
return true;
|
|
2358
|
+
});
|
|
2359
|
+
if (options.near) {
|
|
2360
|
+
const { x, y } = options.near;
|
|
2361
|
+
candidates.sort(
|
|
2362
|
+
(a, b) => Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) - Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y)
|
|
2363
|
+
);
|
|
2364
|
+
} else {
|
|
2365
|
+
candidates.sort((a, b) => {
|
|
2366
|
+
if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
|
|
2367
|
+
return a.node.geometry.y - b.node.geometry.y;
|
|
2368
|
+
}
|
|
2369
|
+
return a.node.geometry.x - b.node.geometry.x;
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
const total = candidates.length;
|
|
2373
|
+
if (total === 0) return { matched: 0, total: 0 };
|
|
2374
|
+
let idx = 0;
|
|
2375
|
+
if (options.nth != null) {
|
|
2376
|
+
idx = options.nth < 0 ? total + options.nth : options.nth;
|
|
2377
|
+
if (idx < 0 || idx >= total) {
|
|
2378
|
+
return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
|
|
2379
|
+
}
|
|
2380
|
+
} else if (total > 1 && !options.first && !options.near) {
|
|
2381
|
+
return {
|
|
2382
|
+
ambiguous: true,
|
|
2383
|
+
total,
|
|
2384
|
+
candidates: candidates.slice(0, 10).map((candidate, index) => ({
|
|
2385
|
+
idx: index,
|
|
2386
|
+
nodeId: candidate.node.nodeId,
|
|
2387
|
+
type: candidate.node.type,
|
|
2388
|
+
testID: candidate.node.testID ?? null,
|
|
2389
|
+
text: nodeText(candidate.node).slice(0, 60),
|
|
2390
|
+
abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
|
|
2391
|
+
layout: {
|
|
2392
|
+
width: Math.round(candidate.node.geometry.width),
|
|
2393
|
+
height: Math.round(candidate.node.geometry.height)
|
|
2394
|
+
},
|
|
2395
|
+
ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
|
|
2396
|
+
}))
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
const picked = candidates[idx];
|
|
2400
|
+
if (!picked) return { matched: 0, total };
|
|
2401
|
+
const node = picked.node;
|
|
2402
|
+
let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
|
|
2403
|
+
mode: "text",
|
|
2404
|
+
value: nodeText(node) || query
|
|
2405
|
+
});
|
|
2406
|
+
if (!resolved) {
|
|
2407
|
+
resolved = {
|
|
2408
|
+
x: node.geometry.x + node.geometry.width / 2,
|
|
2409
|
+
y: node.geometry.y + node.geometry.height / 2,
|
|
2410
|
+
testID: node.testID ?? null,
|
|
2411
|
+
text: nodeText(node) || query,
|
|
2412
|
+
type: node.type
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
return payloadFromResolved(resolved, node, { total, idx });
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
async function tapBest(bridge, query, opts = {}) {
|
|
2420
|
+
return tapResolvedTarget(bridge, {
|
|
2421
|
+
agent: opts.agent,
|
|
2422
|
+
timing: opts.timing,
|
|
2423
|
+
textFallback: query,
|
|
2424
|
+
resolve: async () => {
|
|
2425
|
+
const byId = await resolveTargetCoords(bridge, { mode: "testid", value: query });
|
|
2426
|
+
if (byId) {
|
|
2427
|
+
return {
|
|
2428
|
+
...payloadFromResolved(byId, byId),
|
|
2429
|
+
strategy: "testid",
|
|
2430
|
+
node: {
|
|
2431
|
+
nodeId: byId.nodeId ?? null,
|
|
2432
|
+
id: byId.testID ?? query,
|
|
2433
|
+
testID: byId.testID ?? query,
|
|
2434
|
+
type: byId.type ?? null,
|
|
2435
|
+
text: byId.text ?? null
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
|
|
2440
|
+
if (byText) {
|
|
2441
|
+
return {
|
|
2442
|
+
...payloadFromResolved(byText, byText),
|
|
2443
|
+
strategy: "text",
|
|
2444
|
+
node: {
|
|
2445
|
+
nodeId: byText.nodeId ?? null,
|
|
2446
|
+
id: byText.testID ?? null,
|
|
2447
|
+
testID: byText.testID ?? null,
|
|
2448
|
+
type: byText.type ?? null,
|
|
2449
|
+
text: byText.text ?? query
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
return { strategy: "none" };
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// src/app-url.ts
|
|
2459
|
+
var APP_URL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
2460
|
+
function normalizeAppUrlTarget(target) {
|
|
2461
|
+
const trimmed = target.trim();
|
|
2462
|
+
if (!trimmed) {
|
|
2463
|
+
throw new Error("expected <url-or-route>");
|
|
2464
|
+
}
|
|
2465
|
+
if (APP_URL_SCHEME_RE.test(trimmed) || trimmed.startsWith("/")) {
|
|
2466
|
+
return trimmed;
|
|
2467
|
+
}
|
|
2468
|
+
return `/${trimmed}`;
|
|
2469
|
+
}
|
|
2470
|
+
async function openAppUrl(bridge, target) {
|
|
2471
|
+
const normalized = normalizeAppUrlTarget(target);
|
|
2472
|
+
const result = await bridge.send({
|
|
2473
|
+
type: "openUrl",
|
|
2474
|
+
url: normalized
|
|
2475
|
+
});
|
|
2476
|
+
if (result !== null && typeof result === "object" && Reflect.get(result, "ok") === false) {
|
|
2477
|
+
const error = Reflect.get(result, "error");
|
|
2478
|
+
throw new Error(typeof error === "string" ? error : "deep-link dispatch failed");
|
|
2479
|
+
}
|
|
2480
|
+
return normalized;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// src/sim-client.ts
|
|
2484
|
+
var SimClientError = class extends Error {
|
|
2485
|
+
constructor(message) {
|
|
2486
|
+
super(message);
|
|
2487
|
+
this.name = new.target.name;
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
var SimConnectError = class extends SimClientError {
|
|
2491
|
+
};
|
|
2492
|
+
var SimReadyTimeoutError = class extends SimClientError {
|
|
2493
|
+
status;
|
|
2494
|
+
constructor(status) {
|
|
2495
|
+
super(`sim not ready: ${waitReadyReason(status)}`);
|
|
2496
|
+
this.status = status;
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
var SimTapError = class extends SimClientError {
|
|
2500
|
+
outcome;
|
|
2501
|
+
constructor(message, outcome = null) {
|
|
2502
|
+
super(message);
|
|
2503
|
+
this.outcome = outcome;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
var SimKeyboardError = class extends SimClientError {
|
|
2507
|
+
};
|
|
2508
|
+
var SimPerformError = class extends SimClientError {
|
|
2509
|
+
result;
|
|
2510
|
+
constructor(result) {
|
|
2511
|
+
const failed = result.steps.find((step) => !step.ok);
|
|
2512
|
+
const at = failed ? `${failed.index} (${failed.type})` : String(result.completed);
|
|
2513
|
+
const cause = failed?.error ?? result.error;
|
|
2514
|
+
super(`perform failed at step ${at}${cause ? `: ${cause}` : ""}`);
|
|
2515
|
+
this.result = result;
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
var SimResetError = class extends SimClientError {
|
|
2519
|
+
result;
|
|
2520
|
+
constructor(result) {
|
|
2521
|
+
super(`reset (${result.strategy}) failed${result.error ? `: ${result.error}` : ""}`);
|
|
2522
|
+
this.result = result;
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
function sleep(ms) {
|
|
2526
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2527
|
+
}
|
|
2528
|
+
function decodePngDataUrl(dataUrl) {
|
|
2529
|
+
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:image/png;base64,")) {
|
|
2530
|
+
throw new SimClientError(
|
|
2531
|
+
`screenshot bridge returned a non-png payload: ${JSON.stringify(
|
|
2532
|
+
typeof dataUrl === "string" ? dataUrl.slice(0, 80) : dataUrl
|
|
2533
|
+
)}`
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
const base64 = dataUrl.slice("data:image/png;base64,".length);
|
|
2537
|
+
const binary = atob(base64);
|
|
2538
|
+
const bytes = new Uint8Array(binary.length);
|
|
2539
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
2540
|
+
return { dataUrl, bytes };
|
|
2541
|
+
}
|
|
2542
|
+
var TARGET_SEARCH_PARAMS = [
|
|
2543
|
+
"bundle",
|
|
2544
|
+
"demo",
|
|
2545
|
+
"app",
|
|
2546
|
+
"open",
|
|
2547
|
+
"port",
|
|
2548
|
+
"inspectOpen",
|
|
2549
|
+
"appFonts",
|
|
2550
|
+
"appSplash"
|
|
2551
|
+
];
|
|
2552
|
+
function buildSimOpenUrl(currentUrl, target) {
|
|
2553
|
+
const url = new URL(currentUrl);
|
|
2554
|
+
for (const param of TARGET_SEARCH_PARAMS) url.searchParams.delete(param);
|
|
2555
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
2556
|
+
const prefix2 = pathname === "/__soot" || pathname.startsWith("/__soot/") ? "/__soot" : "";
|
|
2557
|
+
const normalized = String(target).trim();
|
|
2558
|
+
if (/^\d+$/.test(normalized)) {
|
|
2559
|
+
url.pathname = `${prefix2}/rn/${normalized}`;
|
|
2560
|
+
} else {
|
|
2561
|
+
url.pathname = `${prefix2}/rn`;
|
|
2562
|
+
url.searchParams.set("open", normalized);
|
|
2563
|
+
}
|
|
2564
|
+
return url.toString();
|
|
2565
|
+
}
|
|
2566
|
+
function isTextTapTarget(target) {
|
|
2567
|
+
return typeof target === "object" && "text" in target && typeof target.text === "string" && !("layout" in target) && !("absolutePosition" in target);
|
|
2568
|
+
}
|
|
2569
|
+
var SimClient = class {
|
|
2570
|
+
simId;
|
|
2571
|
+
bridge;
|
|
2572
|
+
closed = false;
|
|
2573
|
+
constructor(bridge, simId, _options = {}) {
|
|
2574
|
+
this.bridge = bridge;
|
|
2575
|
+
this.simId = simId;
|
|
2576
|
+
}
|
|
2577
|
+
get plane() {
|
|
2578
|
+
return this.bridge.plane;
|
|
2579
|
+
}
|
|
2580
|
+
// --- transport (WsBridge/InspectBridge) ---
|
|
2581
|
+
send(cmd, opts) {
|
|
2582
|
+
if (this.closed) {
|
|
2583
|
+
return Promise.reject(new SimConnectError("sim client is closed"));
|
|
2584
|
+
}
|
|
2585
|
+
const payload = cmd.simId === void 0 ? { ...cmd, simId: this.simId } : cmd;
|
|
2586
|
+
return this.bridge.send(payload, opts);
|
|
2587
|
+
}
|
|
2588
|
+
listSims() {
|
|
2589
|
+
return this.bridge.listSims();
|
|
2590
|
+
}
|
|
2591
|
+
resolveReloadedSim(retiredSimId) {
|
|
2592
|
+
return this.bridge.resolveReloadedSim(retiredSimId);
|
|
2593
|
+
}
|
|
2594
|
+
focusSim(simId = this.simId) {
|
|
2595
|
+
return this.bridge.focusSim(simId);
|
|
2596
|
+
}
|
|
2597
|
+
closeSim(simId = this.simId) {
|
|
2598
|
+
return this.bridge.closeSim(simId);
|
|
2599
|
+
}
|
|
2600
|
+
claim(simId = this.simId, opts) {
|
|
2601
|
+
return this.bridge.claim(simId, opts);
|
|
2602
|
+
}
|
|
2603
|
+
/** release the socket. a script that connected must close to exit cleanly. */
|
|
2604
|
+
close() {
|
|
2605
|
+
if (this.closed) return;
|
|
2606
|
+
this.closed = true;
|
|
2607
|
+
this.bridge.close();
|
|
2608
|
+
}
|
|
2609
|
+
// --- navigation + readiness ---
|
|
2610
|
+
/**
|
|
2611
|
+
* point this sim at a dev target: a metro port (`8081`), or anything the
|
|
2612
|
+
* shell's connect flow accepts (`localhost:8081`, an app slug…). resolves
|
|
2613
|
+
* once the reloaded page answers on the new target route.
|
|
2614
|
+
*/
|
|
2615
|
+
async open(target, opts = {}) {
|
|
2616
|
+
const sims = await this.listSims();
|
|
2617
|
+
const sim = sims.find((entry) => entry.id === this.simId);
|
|
2618
|
+
if (!sim) {
|
|
2619
|
+
throw new SimConnectError(`sim ${this.simId} is no longer connected`);
|
|
2620
|
+
}
|
|
2621
|
+
const currentUrl = sim.url ?? sim.origin;
|
|
2622
|
+
if (!currentUrl) {
|
|
2623
|
+
throw new SimConnectError(
|
|
2624
|
+
`sim ${this.simId} reported no URL to derive the shell from`
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
const nextUrl = new URL(buildSimOpenUrl(currentUrl, target));
|
|
2628
|
+
const token = `sim-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2629
|
+
nextUrl.searchParams.set("inspectOpen", token);
|
|
2630
|
+
await this.send({
|
|
2631
|
+
type: "evaluate",
|
|
2632
|
+
code: `window.location.href = ${JSON.stringify(nextUrl.toString())}`
|
|
2633
|
+
});
|
|
2634
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
2635
|
+
while (Date.now() < deadline) {
|
|
2636
|
+
await sleep(250);
|
|
2637
|
+
try {
|
|
2638
|
+
const { url } = await inspectUrl(this);
|
|
2639
|
+
if (new URL(url).searchParams.get("inspectOpen") === token) return;
|
|
2640
|
+
} catch {
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
throw new SimConnectError(
|
|
2644
|
+
`timed out waiting for sim ${this.simId} to load ${nextUrl}`
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
/** dispatch a route or app URL through React Native Linking in this guest. */
|
|
2648
|
+
openUrl(target) {
|
|
2649
|
+
return openAppUrl(this, target);
|
|
2650
|
+
}
|
|
2651
|
+
async setAppearance(appearance) {
|
|
2652
|
+
await this.send({ type: "setAppearance", appearance });
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* block until the guest app has mounted and painted real content.
|
|
2656
|
+
* throws SimReadyTimeoutError (with the last probe status) on timeout.
|
|
2657
|
+
*/
|
|
2658
|
+
async waitReady(opts = {}) {
|
|
2659
|
+
const status = await inspectWaitReady(this, opts.timeoutMs ?? 2e4, {
|
|
2660
|
+
onProgress: opts.onProgress
|
|
2661
|
+
});
|
|
2662
|
+
if (!status.ready) throw new SimReadyTimeoutError(status);
|
|
2663
|
+
return status;
|
|
2664
|
+
}
|
|
2665
|
+
/** wait for layout/animation/transition settle after an interaction. */
|
|
2666
|
+
settle(opts = {}) {
|
|
2667
|
+
return waitForSootsimIdle({
|
|
2668
|
+
bridge: this,
|
|
2669
|
+
maxMs: opts.maxMs ?? 3e3,
|
|
2670
|
+
strict: opts.strict
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
// --- read verbs ---
|
|
2674
|
+
/** first matching node, or null. query precedence matches the CLI's find. */
|
|
2675
|
+
async find(query) {
|
|
2676
|
+
const nodes = await this.findAll(query);
|
|
2677
|
+
return nodes[0] ?? null;
|
|
2678
|
+
}
|
|
2679
|
+
async findAll(query) {
|
|
2680
|
+
const mapped = {
|
|
2681
|
+
testId: query.testId ?? null,
|
|
2682
|
+
role: query.role ?? null,
|
|
2683
|
+
type: query.type ?? null,
|
|
2684
|
+
text: query.text ?? null,
|
|
2685
|
+
pressable: query.pressable
|
|
2686
|
+
};
|
|
2687
|
+
const found = await inspectFind(this, mapped);
|
|
2688
|
+
if (!found) {
|
|
2689
|
+
throw new SimClientError(
|
|
2690
|
+
"find() needs at least one of testId, text, role, type, or pressable"
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
const { result } = found;
|
|
2694
|
+
if (Array.isArray(result)) return result;
|
|
2695
|
+
return result ? [result] : [];
|
|
2696
|
+
}
|
|
2697
|
+
async tree(depth = 5) {
|
|
2698
|
+
const { tree } = await inspectTree(this, depth);
|
|
2699
|
+
return Array.isArray(tree) ? tree : [];
|
|
2700
|
+
}
|
|
2701
|
+
resolve(selector) {
|
|
2702
|
+
return this.send({ type: "resolve", selector });
|
|
2703
|
+
}
|
|
2704
|
+
/** compact visible semantic tree, formatted as the CLI prints it. */
|
|
2705
|
+
describe(opts = {}) {
|
|
2706
|
+
return inspectDescribe(this, {
|
|
2707
|
+
describe: true,
|
|
2708
|
+
verbose: opts.verbose ?? false,
|
|
2709
|
+
filter: opts.filter ?? "",
|
|
2710
|
+
compact: !opts.verbose,
|
|
2711
|
+
hideXy: false,
|
|
2712
|
+
fullText: true
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
accessibilityTree() {
|
|
2716
|
+
return inspectAccessibilityTree(this);
|
|
2717
|
+
}
|
|
2718
|
+
async logs(opts = {}) {
|
|
2719
|
+
return filterLogEntries(await inspectLogs(this), opts);
|
|
2720
|
+
}
|
|
2721
|
+
errors(limit = 20) {
|
|
2722
|
+
return inspectErrors(this, limit);
|
|
2723
|
+
}
|
|
2724
|
+
warnings(limit = 20) {
|
|
2725
|
+
return inspectWarnings(this, limit);
|
|
2726
|
+
}
|
|
2727
|
+
keyboard() {
|
|
2728
|
+
return inspectKeyboard(this);
|
|
2729
|
+
}
|
|
2730
|
+
shellState(readyTimeoutMs = 0) {
|
|
2731
|
+
return getShellState(this, readyTimeoutMs);
|
|
2732
|
+
}
|
|
2733
|
+
async url() {
|
|
2734
|
+
const { url } = await inspectUrl(this);
|
|
2735
|
+
return url;
|
|
2736
|
+
}
|
|
2737
|
+
async screenshot(opts = {}) {
|
|
2738
|
+
const result = await this.send({
|
|
2739
|
+
type: "screenshot",
|
|
2740
|
+
...opts.layers ? { layers: opts.layers } : {},
|
|
2741
|
+
...opts.crop ? { crop: opts.crop } : {}
|
|
2742
|
+
});
|
|
2743
|
+
return decodePngDataUrl(result);
|
|
2744
|
+
}
|
|
2745
|
+
// --- input verbs ---
|
|
2746
|
+
/**
|
|
2747
|
+
* tap by testID, visible text, node, or coordinates. resolution, retry, and
|
|
2748
|
+
* hit verification are the CLI's shared tap kernels. throws SimTapError when
|
|
2749
|
+
* the target is missing, covered, offscreen, or the press missed.
|
|
2750
|
+
*/
|
|
2751
|
+
async tap(target) {
|
|
2752
|
+
if (typeof target === "string") {
|
|
2753
|
+
return this.throwUnlessTapped(await tapBest(this, target), target);
|
|
2754
|
+
}
|
|
2755
|
+
if ("testId" in target && typeof target.testId === "string") {
|
|
2756
|
+
return this.throwUnlessTapped(await tapById(this, target.testId), target.testId);
|
|
2757
|
+
}
|
|
2758
|
+
if (isTextTapTarget(target)) {
|
|
2759
|
+
const { text, ...textOpts } = target;
|
|
2760
|
+
return this.throwUnlessTapped(await tapByText(this, text, textOpts), text);
|
|
2761
|
+
}
|
|
2762
|
+
if ("testID" in target && typeof target.testID === "string" && typeof target.nodeId !== "number") {
|
|
2763
|
+
return this.throwUnlessTapped(await tapById(this, target.testID), target.testID);
|
|
2764
|
+
}
|
|
2765
|
+
const coords = this.coordsForTapTarget(target);
|
|
2766
|
+
const result = await this.send({
|
|
2767
|
+
type: "tap",
|
|
2768
|
+
x: coords.x,
|
|
2769
|
+
y: coords.y,
|
|
2770
|
+
..."nodeId" in target && typeof target.nodeId === "number" ? { target: { nodeId: target.nodeId } } : {}
|
|
2771
|
+
});
|
|
2772
|
+
if (!isTapSuccess(result)) {
|
|
2773
|
+
throw new SimTapError(
|
|
2774
|
+
`tap at ${coords.x},${coords.y} missed: ${JSON.stringify(result)}`
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
async longPress(target, opts = {}) {
|
|
2779
|
+
const coords = await this.resolveCoords(target);
|
|
2780
|
+
const result = await this.send(
|
|
2781
|
+
{
|
|
2782
|
+
type: "longPress",
|
|
2783
|
+
x: coords.x,
|
|
2784
|
+
y: coords.y,
|
|
2785
|
+
durationMs: opts.durationMs ?? 600
|
|
2786
|
+
},
|
|
2787
|
+
{ timeoutMs: (opts.durationMs ?? 600) + 15e3 }
|
|
2788
|
+
);
|
|
2789
|
+
if (!result?.ok) {
|
|
2790
|
+
throw new SimTapError(`longPress at ${coords.x},${coords.y} missed`);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
/** type through the visual iOS keyboard. requires a focused input. */
|
|
2794
|
+
async type(text) {
|
|
2795
|
+
await this.requireVisibleKeyboard("type");
|
|
2796
|
+
await this.send({ type: "keyboard", action: "type", text });
|
|
2797
|
+
}
|
|
2798
|
+
/** press a keyboard key ('return', 'delete', 'space', …). */
|
|
2799
|
+
async press(key) {
|
|
2800
|
+
await this.requireVisibleKeyboard("press");
|
|
2801
|
+
await this.send({ type: "keyboard", action: "press", text: key });
|
|
2802
|
+
}
|
|
2803
|
+
async dismissKeyboard() {
|
|
2804
|
+
await this.send({ type: "keyboard", action: "dismiss" });
|
|
2805
|
+
}
|
|
2806
|
+
// --- batched input, compound state, reset ---
|
|
2807
|
+
/**
|
|
2808
|
+
* execute an ordered step batch inside the page in ONE round-trip, so waits
|
|
2809
|
+
* and gesture physics run on real wall-clock timing. `tapText` steps are
|
|
2810
|
+
* resolved to coordinates client-side before the batch is sent. throws
|
|
2811
|
+
* SimPerformError (carrying the per-step result) when the batch fails.
|
|
2812
|
+
*/
|
|
2813
|
+
async perform(steps, options) {
|
|
2814
|
+
const resolved = [];
|
|
2815
|
+
for (const step of steps) {
|
|
2816
|
+
if (step.type !== "tapText") {
|
|
2817
|
+
resolved.push(step);
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
const target = await resolveTargetCoords(this, {
|
|
2821
|
+
mode: "text",
|
|
2822
|
+
value: step.text
|
|
2823
|
+
});
|
|
2824
|
+
if (!target) {
|
|
2825
|
+
throw new SimTapError(
|
|
2826
|
+
`perform tapText: no node with text ${JSON.stringify(step.text)}`
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
resolved.push({
|
|
2830
|
+
type: "tap",
|
|
2831
|
+
x: target.x,
|
|
2832
|
+
y: target.y,
|
|
2833
|
+
target: {
|
|
2834
|
+
nodeId: target.nodeId,
|
|
2835
|
+
testID: target.testID ?? void 0,
|
|
2836
|
+
text: target.text ?? void 0,
|
|
2837
|
+
type: target.type ?? void 0
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
const batchTimeoutMs = options?.timeoutMs ?? 3e4;
|
|
2842
|
+
const result = await this.send(
|
|
2843
|
+
{
|
|
2844
|
+
type: "perform",
|
|
2845
|
+
steps: resolved,
|
|
2846
|
+
...options ? { performOptions: options } : {}
|
|
2847
|
+
},
|
|
2848
|
+
{ timeoutMs: batchTimeoutMs + 5e3 }
|
|
2849
|
+
);
|
|
2850
|
+
if (!result?.ok) throw new SimPerformError(result);
|
|
2851
|
+
return result;
|
|
2852
|
+
}
|
|
2853
|
+
/** screenshot + tree + route + recent errors in one round-trip. */
|
|
2854
|
+
state(options) {
|
|
2855
|
+
return this.send({
|
|
2856
|
+
type: "state",
|
|
2857
|
+
...options ? { stateOptions: options } : {}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* two-tier app state wipe ('data' clears storage/caches, 'full' also clears
|
|
2862
|
+
* keychain/permissions/native-module state). throws SimResetError on failure.
|
|
2863
|
+
*/
|
|
2864
|
+
async reset(options) {
|
|
2865
|
+
const result = await this.send(
|
|
2866
|
+
{ type: "reset", ...options ? { resetOptions: options } : {} },
|
|
2867
|
+
{ timeoutMs: 6e4 }
|
|
2868
|
+
);
|
|
2869
|
+
if (!result?.ok) throw new SimResetError(result);
|
|
2870
|
+
return result;
|
|
2871
|
+
}
|
|
2872
|
+
// --- internals ---
|
|
2873
|
+
throwUnlessTapped(outcome, label) {
|
|
2874
|
+
if (!outcome.failure && isTapSuccess(outcome.result)) return;
|
|
2875
|
+
const reason = outcome.failure === "not-found" ? "no matching node" : outcome.failure === "missed" ? outcome.result?.reason ?? "press missed" : outcome.payload?.ambiguous ? `ambiguous: ${outcome.payload.total} matches` : outcome.result?.reason ?? outcome.payload?.error ?? "tap failed";
|
|
2876
|
+
throw new SimTapError(`tap ${JSON.stringify(label)} failed: ${reason}`, outcome);
|
|
2877
|
+
}
|
|
2878
|
+
coordsForTapTarget(target) {
|
|
2879
|
+
if (typeof target === "object" && "x" in target && "y" in target && typeof target.x === "number" && typeof target.y === "number") {
|
|
2880
|
+
return { x: target.x, y: target.y };
|
|
2881
|
+
}
|
|
2882
|
+
if (typeof target === "object" && "absolutePosition" in target && target.absolutePosition && target.layout) {
|
|
2883
|
+
return {
|
|
2884
|
+
x: target.absolutePosition.x + (target.layout.width ?? 0) / 2,
|
|
2885
|
+
y: target.absolutePosition.y + (target.layout.height ?? 0) / 2
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
throw new SimClientError(
|
|
2889
|
+
"tap target needs coordinates, a testId, text, or a node from find()"
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
async resolveCoords(target) {
|
|
2893
|
+
if (typeof target === "string") {
|
|
2894
|
+
const resolved = await resolveTargetCoords(this, { mode: "testid", value: target }) ?? await resolveTargetCoords(this, { mode: "text", value: target });
|
|
2895
|
+
if (!resolved) throw new SimTapError(`no node matching ${JSON.stringify(target)}`);
|
|
2896
|
+
return resolved;
|
|
2897
|
+
}
|
|
2898
|
+
if ("testId" in target && typeof target.testId === "string") {
|
|
2899
|
+
const resolved = await resolveTargetCoords(this, {
|
|
2900
|
+
mode: "testid",
|
|
2901
|
+
value: target.testId
|
|
2902
|
+
});
|
|
2903
|
+
if (!resolved) {
|
|
2904
|
+
throw new SimTapError(`no node with testID ${JSON.stringify(target.testId)}`);
|
|
2905
|
+
}
|
|
2906
|
+
return resolved;
|
|
2907
|
+
}
|
|
2908
|
+
if ("text" in target && typeof target.text === "string" && !("layout" in target)) {
|
|
2909
|
+
const resolved = await resolveTargetCoords(this, {
|
|
2910
|
+
mode: "text",
|
|
2911
|
+
value: target.text
|
|
2912
|
+
});
|
|
2913
|
+
if (!resolved) {
|
|
2914
|
+
throw new SimTapError(`no node with text ${JSON.stringify(target.text)}`);
|
|
2915
|
+
}
|
|
2916
|
+
return resolved;
|
|
2917
|
+
}
|
|
2918
|
+
return this.coordsForTapTarget(target);
|
|
2919
|
+
}
|
|
2920
|
+
async requireVisibleKeyboard(action) {
|
|
2921
|
+
const deadline = Date.now() + 1500;
|
|
2922
|
+
for (; ; ) {
|
|
2923
|
+
const state = await inspectKeyboard(this);
|
|
2924
|
+
if (!("error" in state) && state.visible) return;
|
|
2925
|
+
if (Date.now() >= deadline) {
|
|
2926
|
+
throw new SimKeyboardError(
|
|
2927
|
+
`${action} requires the iOS keyboard to be visible \u2014 focus an input first (tap a TextInput)`
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
await sleep(150);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2934
|
+
|
|
2935
|
+
// src/connect.ts
|
|
2936
|
+
function pickSim(sims, wanted, port) {
|
|
2937
|
+
const open = sims.filter((sim) => sim.readyState === "open");
|
|
2938
|
+
if (wanted) {
|
|
2939
|
+
const match = open.find((sim) => sim.id === wanted);
|
|
2940
|
+
if (!match) {
|
|
2941
|
+
throw new SimConnectError(
|
|
2942
|
+
`no sim connected with id ${wanted} on bridge :${port}` + (open.length ? ` \u2014 connected: ${open.map((sim) => sim.id).join(", ")}` : " \u2014 no sims connected; open one with `rnx open <port>`")
|
|
2943
|
+
);
|
|
2944
|
+
}
|
|
2945
|
+
return match;
|
|
2946
|
+
}
|
|
2947
|
+
const pinned = readCurrentSimId();
|
|
2948
|
+
if (pinned) {
|
|
2949
|
+
const match = open.find((sim) => sim.id === pinned);
|
|
2950
|
+
if (match) return match;
|
|
2951
|
+
}
|
|
2952
|
+
const primary = open.find((sim) => sim.isPrimary) ?? open[0];
|
|
2953
|
+
if (!primary) {
|
|
2954
|
+
throw new SimConnectError(
|
|
2955
|
+
`no sims connected on bridge :${port} \u2014 open one with \`rnx open <port>\``
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
return primary;
|
|
2959
|
+
}
|
|
2960
|
+
async function connect(options = {}) {
|
|
2961
|
+
const target = {
|
|
2962
|
+
simId: options.sim,
|
|
2963
|
+
simIdSource: options.sim ? "flag" : "none"
|
|
2964
|
+
};
|
|
2965
|
+
const cloud = resolveCloudSessionForParsed(target);
|
|
2966
|
+
if (cloud) {
|
|
2967
|
+
const bridge2 = createCloudBridgeForParsed(target);
|
|
2968
|
+
if (!bridge2)
|
|
2969
|
+
throw new SimConnectError("remote simulator session disappeared while connecting");
|
|
2970
|
+
return new SimClient(bridge2, cloud.simId, {
|
|
2971
|
+
commandTimeoutMs: options.timeoutMs ?? 15e3
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
const port = options.port ?? resolveBridgeWorld().bridgePort;
|
|
2975
|
+
const commandTimeoutMs = options.timeoutMs ?? 15e3;
|
|
2976
|
+
const discovery = createBridge(port, { commandTimeoutMs, cliLabel: "sdk" });
|
|
2977
|
+
let picked;
|
|
2978
|
+
try {
|
|
2979
|
+
picked = pickSim(await discovery.listSims(), options.sim, port);
|
|
2980
|
+
} catch (error) {
|
|
2981
|
+
discovery.close();
|
|
2982
|
+
if (error instanceof SimConnectError) throw error;
|
|
2983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2984
|
+
throw new SimConnectError(`${message} \u2014 is the rnx daemon or dev shell running?`);
|
|
2985
|
+
}
|
|
2986
|
+
discovery.close();
|
|
2987
|
+
const bridge = createBridge(port, {
|
|
2988
|
+
commandTimeoutMs,
|
|
2989
|
+
cliLabel: "sdk",
|
|
2990
|
+
simId: picked.id,
|
|
2991
|
+
simIdSource: "flag"
|
|
2992
|
+
});
|
|
2993
|
+
return new SimClient(bridge, picked.id, { commandTimeoutMs });
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
// src/capture-contract.ts
|
|
2997
|
+
var RNX_SCREEN_CAPTURE_VERSION = 1;
|
|
2998
|
+
function selectRnxRasterNodeIds(tree) {
|
|
2999
|
+
const nodesById = new Map(tree.nodes.map((node) => [node.nodeId, node]));
|
|
3000
|
+
const unsupported = /* @__PURE__ */ new Set();
|
|
3001
|
+
for (const node of tree.nodes) {
|
|
3002
|
+
const visibleFrame = node.visibleFrame ?? node.frame;
|
|
3003
|
+
if (!visibleFrame || visibleFrame.width <= 0 || visibleFrame.height <= 0) continue;
|
|
3004
|
+
if (node.type === "masked-view" || node.type === "text" && typeof node.text === "string" && (new RegExp("\\p{Emoji_Presentation}", "u").test(node.text) || new RegExp("\\uFE0F|\\u200D|\\u20E3|\\p{Emoji_Modifier}|\\p{Regional_Indicator}", "u").test(
|
|
3005
|
+
node.text
|
|
3006
|
+
)) || node.style._liquidGlass !== void 0 || node.style._liquidGlassEdge !== void 0) {
|
|
3007
|
+
unsupported.add(node.nodeId);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
for (const nodeId of [...unsupported]) {
|
|
3011
|
+
let parentNodeId = nodesById.get(nodeId)?.parentNodeId ?? null;
|
|
3012
|
+
while (parentNodeId !== null) {
|
|
3013
|
+
if (unsupported.has(parentNodeId)) {
|
|
3014
|
+
unsupported.delete(nodeId);
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
parentNodeId = nodesById.get(parentNodeId)?.parentNodeId ?? null;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
const screenArea = tree.width * tree.height;
|
|
3021
|
+
const maxLayerArea = screenArea * 0.25;
|
|
3022
|
+
const maxAtlasArea = screenArea * 0.5;
|
|
3023
|
+
const candidates = [...unsupported].map((nodeId) => {
|
|
3024
|
+
const node = nodesById.get(nodeId);
|
|
3025
|
+
const frame = node?.visibleFrame ?? node?.frame;
|
|
3026
|
+
return frame ? { nodeId, area: frame.width * frame.height } : null;
|
|
3027
|
+
}).filter(
|
|
3028
|
+
(candidate) => candidate !== null && candidate.area <= maxLayerArea
|
|
3029
|
+
).sort((left, right) => left.area - right.area || left.nodeId - right.nodeId);
|
|
3030
|
+
const selected = /* @__PURE__ */ new Set();
|
|
3031
|
+
let atlasArea = 0;
|
|
3032
|
+
for (const candidate of candidates) {
|
|
3033
|
+
if (atlasArea + candidate.area > maxAtlasArea) continue;
|
|
3034
|
+
selected.add(candidate.nodeId);
|
|
3035
|
+
atlasArea += candidate.area;
|
|
3036
|
+
}
|
|
3037
|
+
return selected;
|
|
3038
|
+
}
|
|
3039
|
+
function rnxRasterFrames(tree, nodeIds) {
|
|
3040
|
+
const frames = /* @__PURE__ */ new Map();
|
|
3041
|
+
for (const node of tree.nodes) {
|
|
3042
|
+
if (!nodeIds.has(node.nodeId)) continue;
|
|
3043
|
+
const frame = node.frame ?? {
|
|
3044
|
+
x: node.absolutePosition.x,
|
|
3045
|
+
y: node.absolutePosition.y,
|
|
3046
|
+
width: node.layout.width,
|
|
3047
|
+
height: node.layout.height
|
|
3048
|
+
};
|
|
3049
|
+
const liquidGlass = node.style._liquidGlass;
|
|
3050
|
+
const capturePadding = liquidGlass && typeof liquidGlass === "object" && typeof Reflect.get(liquidGlass, "capturePadding") === "number" ? Reflect.get(liquidGlass, "capturePadding") : 0;
|
|
3051
|
+
const shadowRadius = typeof node.style.shadowRadius === "number" ? node.style.shadowRadius : 0;
|
|
3052
|
+
const shadowOffset = node.style.shadowOffset;
|
|
3053
|
+
const shadowOffsetX = shadowOffset && typeof shadowOffset === "object" && typeof Reflect.get(shadowOffset, "width") === "number" ? Reflect.get(shadowOffset, "width") : 0;
|
|
3054
|
+
const shadowOffsetY = shadowOffset && typeof shadowOffset === "object" && typeof Reflect.get(shadowOffset, "height") === "number" ? Reflect.get(shadowOffset, "height") : 0;
|
|
3055
|
+
const left = Math.max(
|
|
3056
|
+
0,
|
|
3057
|
+
frame.x - Math.max(capturePadding, shadowRadius - shadowOffsetX)
|
|
3058
|
+
);
|
|
3059
|
+
const top = Math.max(
|
|
3060
|
+
0,
|
|
3061
|
+
frame.y - Math.max(capturePadding, shadowRadius - shadowOffsetY)
|
|
3062
|
+
);
|
|
3063
|
+
const right = Math.min(
|
|
3064
|
+
tree.width,
|
|
3065
|
+
frame.x + frame.width + Math.max(capturePadding, shadowRadius + shadowOffsetX)
|
|
3066
|
+
);
|
|
3067
|
+
const bottom = Math.min(
|
|
3068
|
+
tree.height,
|
|
3069
|
+
frame.y + frame.height + Math.max(capturePadding, shadowRadius + shadowOffsetY)
|
|
3070
|
+
);
|
|
3071
|
+
if (right > left && bottom > top) {
|
|
3072
|
+
frames.set(node.nodeId, {
|
|
3073
|
+
x: left,
|
|
3074
|
+
y: top,
|
|
3075
|
+
width: right - left,
|
|
3076
|
+
height: bottom - top
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
return frames;
|
|
3081
|
+
}
|
|
3082
|
+
function rnxCaptureRasterRegions(capture) {
|
|
3083
|
+
const tree = {
|
|
3084
|
+
width: capture.tree.viewport.width,
|
|
3085
|
+
height: capture.tree.viewport.height,
|
|
3086
|
+
nodes: capture.tree.nodes.map((node) => ({
|
|
3087
|
+
nodeId: node.nodeId,
|
|
3088
|
+
parentNodeId: node.parentNodeId,
|
|
3089
|
+
type: node.type,
|
|
3090
|
+
text: node.text,
|
|
3091
|
+
style: node.style.resolved,
|
|
3092
|
+
layout: node.layout.local,
|
|
3093
|
+
frame: node.layout.frame,
|
|
3094
|
+
absolutePosition: node.layout.absolutePosition,
|
|
3095
|
+
visibleFrame: node.layout.visibleFrame
|
|
3096
|
+
}))
|
|
3097
|
+
};
|
|
3098
|
+
const nodeIds = selectRnxRasterNodeIds(tree);
|
|
3099
|
+
const frames = rnxRasterFrames(tree, nodeIds);
|
|
3100
|
+
return [...nodeIds].flatMap((nodeId) => {
|
|
3101
|
+
const frame = frames.get(nodeId);
|
|
3102
|
+
return frame ? [{ key: String(nodeId), nodeId, frame }] : [];
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
function captureRecord(value) {
|
|
3106
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3107
|
+
}
|
|
3108
|
+
function captureInteger(value) {
|
|
3109
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
3110
|
+
}
|
|
3111
|
+
function captureNumber(value) {
|
|
3112
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3113
|
+
}
|
|
3114
|
+
function captureNullableString(value) {
|
|
3115
|
+
return value === null || typeof value === "string";
|
|
3116
|
+
}
|
|
3117
|
+
function captureRect(value) {
|
|
3118
|
+
return captureRecord(value) && captureNumber(value.x) && captureNumber(value.y) && captureNumber(value.width) && captureNumber(value.height);
|
|
3119
|
+
}
|
|
3120
|
+
function captureOptionalString(value) {
|
|
3121
|
+
return value === void 0 || typeof value === "string";
|
|
3122
|
+
}
|
|
3123
|
+
function captureOptionalNullableString(value) {
|
|
3124
|
+
return value === void 0 || captureNullableString(value);
|
|
3125
|
+
}
|
|
3126
|
+
function captureFontRegistration(value) {
|
|
3127
|
+
return captureRecord(value) && typeof value.family === "string" && value.family.length > 0 && captureOptionalString(value.url) && captureOptionalString(value.sourceLabel) && captureOptionalNullableString(value.parsedFamily) && captureOptionalNullableString(value.parsedPostScript) && captureOptionalNullableString(value.fullName);
|
|
3128
|
+
}
|
|
3129
|
+
function capturePoint(value) {
|
|
3130
|
+
return captureRecord(value) && captureNumber(value.x) && captureNumber(value.y);
|
|
3131
|
+
}
|
|
3132
|
+
function captureNode(value) {
|
|
3133
|
+
if (!captureRecord(value) || !captureInteger(value.nodeId) || !(value.parentNodeId === null || captureInteger(value.parentNodeId)) || !Array.isArray(value.childNodeIds) || !value.childNodeIds.every(captureInteger) || !captureInteger(value.paintIndex) || typeof value.type !== "string" || !captureNullableString(value.id) || !captureNullableString(value.testID) || !captureNullableString(value.text) || !captureInteger(value.childCount) || !captureRecord(value.accessibility) || !captureRecord(value.interaction) || !captureRecord(value.layout) || !captureRecord(value.style) || !captureRecord(value.source) || !captureRecord(value.props) || !captureRecord(value.inspection)) {
|
|
3134
|
+
return false;
|
|
3135
|
+
}
|
|
3136
|
+
const accessibility = value.accessibility;
|
|
3137
|
+
if (typeof accessibility.accessible !== "boolean" || !captureNullableString(accessibility.label) || !captureNullableString(accessibility.role) || !captureNullableString(accessibility.hint) || !(accessibility.liveRegion === null || accessibility.liveRegion === "none" || accessibility.liveRegion === "polite" || accessibility.liveRegion === "assertive") || !(accessibility.state === null || captureRecord(accessibility.state)) || !(accessibility.value === null || captureRecord(accessibility.value))) {
|
|
3138
|
+
return false;
|
|
3139
|
+
}
|
|
3140
|
+
const interaction = value.interaction;
|
|
3141
|
+
if (typeof interaction.pressed !== "boolean" || typeof interaction.pressable !== "boolean" || typeof interaction.textInput !== "boolean" || !(interaction.textSelection === null || captureRecord(interaction.textSelection)) || typeof interaction.secureTextEntry !== "boolean" || !captureNullableString(interaction.placeholder) || !capturePoint(interaction.scrollOffset) || !(interaction.zoomScale === null || captureNumber(interaction.zoomScale)) || typeof interaction.nativePinchEnabled !== "boolean" || !(interaction.contentOffsetProp === null || captureRecord(interaction.contentOffsetProp)) || !(interaction.strokeDashoffsetProp === null || captureNumber(interaction.strokeDashoffsetProp))) {
|
|
3142
|
+
return false;
|
|
3143
|
+
}
|
|
3144
|
+
const layout = value.layout;
|
|
3145
|
+
if (!captureRect(layout.local) || !(layout.frame === null || captureRect(layout.frame)) || !capturePoint(layout.absolutePosition) || !(layout.visibleFrame === null || captureRect(layout.visibleFrame))) {
|
|
3146
|
+
return false;
|
|
3147
|
+
}
|
|
3148
|
+
const style = value.style;
|
|
3149
|
+
return captureRecord(style.resolved) && captureRecord(style.authored) && captureRecord(style.computed) && captureRecord(style.tokenBindings) && (value.textLines === void 0 || Array.isArray(value.textLines));
|
|
3150
|
+
}
|
|
3151
|
+
function isRnxScreenCapture(value) {
|
|
3152
|
+
if (!captureRecord(value) || value.contractVersion !== RNX_SCREEN_CAPTURE_VERSION || !captureInteger(value.capturedAt) || !captureRecord(value.tree) || !(value.fonts === void 0 || Array.isArray(value.fonts) && value.fonts.length <= 200 && value.fonts.every(captureFontRegistration)) || !(value.visual === void 0 || captureRecord(value.visual))) {
|
|
3153
|
+
return false;
|
|
3154
|
+
}
|
|
3155
|
+
const tree = value.tree;
|
|
3156
|
+
const visual = value.visual;
|
|
3157
|
+
return captureRecord(tree.viewport) && captureInteger(tree.viewport.width) && tree.viewport.width > 0 && captureInteger(tree.viewport.height) && tree.viewport.height > 0 && captureInteger(tree.rootNodeId) && (tree.commitRevision === void 0 || captureInteger(tree.commitRevision)) && Array.isArray(tree.nodes) && tree.nodes.every(captureNode) && (visual === void 0 || visual.kind === "png" && (visual.producer === "headless-cpu-app-surface" || visual.producer === "browser-compositor-app-surface") && typeof visual.dataUri === "string" && visual.dataUri.startsWith("data:image/png;base64,") && captureInteger(visual.pixelWidth) && visual.pixelWidth > 0 && captureInteger(visual.pixelHeight) && visual.pixelHeight > 0);
|
|
3158
|
+
}
|
|
3159
|
+
function isRnxCaptureRegionAtlas(value) {
|
|
3160
|
+
if (!captureRecord(value) || value.kind !== "png-atlas" || value.producer !== "headless-cpu-app-surface" || typeof value.dataUri !== "string" || !value.dataUri.startsWith("data:image/png;base64,") || !captureNumber(value.width) || value.width <= 0 || !captureNumber(value.height) || value.height <= 0 || !captureInteger(value.pixelWidth) || value.pixelWidth <= 0 || !captureInteger(value.pixelHeight) || value.pixelHeight <= 0 || !Array.isArray(value.regions) || value.regions.length === 0 || value.regions.length > 256) {
|
|
3161
|
+
return false;
|
|
3162
|
+
}
|
|
3163
|
+
const scale = value.pixelWidth / value.width;
|
|
3164
|
+
if (!Number.isInteger(scale) || scale < 1 || scale > 4 || Math.abs(value.height * scale - value.pixelHeight) > 1e-6) {
|
|
3165
|
+
return false;
|
|
3166
|
+
}
|
|
3167
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3168
|
+
for (const region of value.regions) {
|
|
3169
|
+
if (!captureRecord(region) || typeof region.key !== "string" || region.key.length === 0 || keys.has(region.key) || !captureInteger(region.nodeId) || region.nodeId <= 0 || !captureRect(region.frame) || region.frame.width <= 0 || region.frame.height <= 0 || !captureRect(region.source) || region.source.x < 0 || region.source.y < 0 || region.source.width !== region.frame.width || region.source.height !== region.frame.height || region.source.x + region.source.width > value.width || region.source.y + region.source.height > value.height) {
|
|
3170
|
+
return false;
|
|
3171
|
+
}
|
|
3172
|
+
keys.add(region.key);
|
|
3173
|
+
}
|
|
3174
|
+
return true;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// src/vite-plugin-one.ts
|
|
3178
|
+
var import_fs4 = __toESM(require("fs"), 1);
|
|
3179
|
+
var import_node_crypto4 = require("node:crypto");
|
|
3180
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
3181
|
+
|
|
3182
|
+
// src/dev-server-open.ts
|
|
3183
|
+
var import_node_child_process = require("node:child_process");
|
|
3184
|
+
var import_node_crypto3 = require("node:crypto");
|
|
3185
|
+
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
3186
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
3187
|
+
var import_node_url = require("node:url");
|
|
3188
|
+
function resolveRnxDesktopAppName(open, projectRoot) {
|
|
3189
|
+
if (!open) return null;
|
|
3190
|
+
if (typeof open === "object") {
|
|
3191
|
+
const appName = open.appName.trim();
|
|
3192
|
+
if (!appName) throw new Error("rnx: open.appName must not be empty");
|
|
3193
|
+
return appName;
|
|
3194
|
+
}
|
|
3195
|
+
const packageJsonPath = import_node_path4.default.join(projectRoot, "package.json");
|
|
3196
|
+
const packageJson = JSON.parse(import_node_fs4.default.readFileSync(packageJsonPath, "utf8"));
|
|
3197
|
+
const packageName = packageJson && typeof packageJson === "object" ? Reflect.get(packageJson, "name") : void 0;
|
|
3198
|
+
if (typeof packageName !== "string" || !packageName.trim()) {
|
|
3199
|
+
throw new Error(`rnx: ${packageJsonPath} must declare a package name`);
|
|
3200
|
+
}
|
|
3201
|
+
return packageName.trim();
|
|
3202
|
+
}
|
|
3203
|
+
function tagRnxBundleForPlugin(bundleUrl, replacementOrigin, replacementToken, projectRoot, appName) {
|
|
3204
|
+
const absolute = /^[a-z][a-z\d+.-]*:\/\//i.test(bundleUrl);
|
|
3205
|
+
const url = new URL(bundleUrl, absolute ? void 0 : "http://rnx.local");
|
|
3206
|
+
if (appName) {
|
|
3207
|
+
url.searchParams.set("rnxAppName", appName);
|
|
3208
|
+
url.searchParams.set(
|
|
3209
|
+
"rnxProjectId",
|
|
3210
|
+
(0, import_node_crypto3.createHash)("sha256").update(import_node_fs4.default.realpathSync(projectRoot)).digest("hex").slice(0, 32)
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
url.searchParams.set("rnxReplacementOrigin", replacementOrigin);
|
|
3214
|
+
url.searchParams.set("rnxReplacementToken", replacementToken);
|
|
3215
|
+
return absolute ? url.toString() : `${url.pathname}${url.search}${url.hash}`;
|
|
3216
|
+
}
|
|
3217
|
+
function resolveRnxDesktopLaunchTarget(port, bundleUrl) {
|
|
3218
|
+
return new URL(bundleUrl, `http://localhost:${port}`).toString();
|
|
3219
|
+
}
|
|
3220
|
+
function buildRnxDesktopOpenArgs(bundleUrl) {
|
|
3221
|
+
return ["open", bundleUrl, "--new", "--driver", "electron", "--no-describe"];
|
|
3222
|
+
}
|
|
3223
|
+
function observeRnxDesktopLaunch(child, appName) {
|
|
3224
|
+
let stderr = "";
|
|
3225
|
+
let spawnFailed = false;
|
|
3226
|
+
child.stderr?.setEncoding("utf8");
|
|
3227
|
+
child.stderr?.on("data", (chunk) => {
|
|
3228
|
+
stderr = `${stderr}${chunk}`.slice(-16384);
|
|
3229
|
+
});
|
|
3230
|
+
child.once("error", (error) => {
|
|
3231
|
+
spawnFailed = true;
|
|
3232
|
+
console.error(`[rnx] could not open ${appName}: ${error.message}`);
|
|
3233
|
+
});
|
|
3234
|
+
child.once("close", (code, signal) => {
|
|
3235
|
+
if (spawnFailed) return;
|
|
3236
|
+
if (code === 0) {
|
|
3237
|
+
console.log(`[rnx] opened ${appName} in the desktop simulator`);
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
const detail = stderr.trim() || (signal ? `signal ${signal}` : `exit ${code}`);
|
|
3241
|
+
console.error(`[rnx] could not open ${appName}: ${detail}`);
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
function launchRnxDesktop(bundleUrl, appName) {
|
|
3245
|
+
const packageRoot = import_node_path4.default.resolve(import_node_path4.default.dirname((0, import_node_url.fileURLToPath)(__sootsim_import_meta_url)), "..");
|
|
3246
|
+
const binPath = import_node_path4.default.join(packageRoot, "dist-cli", "bin.js");
|
|
3247
|
+
console.log(`[rnx] opening ${appName} in the desktop simulator`);
|
|
3248
|
+
const child = (0, import_node_child_process.spawn)(
|
|
3249
|
+
process.execPath,
|
|
3250
|
+
[binPath, ...buildRnxDesktopOpenArgs(bundleUrl)],
|
|
3251
|
+
{
|
|
3252
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
3253
|
+
windowsHide: true
|
|
3254
|
+
}
|
|
3255
|
+
);
|
|
3256
|
+
observeRnxDesktopLaunch(child, appName);
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
// src/host/replacement-module-handler.ts
|
|
3260
|
+
var import_promises = __toESM(require("node:fs/promises"), 1);
|
|
3261
|
+
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
3262
|
+
|
|
3263
|
+
// ../contrast-bundler/src/replacementModule.ts
|
|
3264
|
+
var import_dist = require("sucrase/dist/index.js");
|
|
3265
|
+
var import_parser = require("sucrase/dist/parser/index.js");
|
|
3266
|
+
var import_types = require("sucrase/dist/parser/tokenizer/types.js");
|
|
3267
|
+
|
|
3268
|
+
// ../contrast-bundler/src/posixPath.ts
|
|
3269
|
+
var sep = "/";
|
|
3270
|
+
var delimiter = ":";
|
|
3271
|
+
function isAbsolute(p) {
|
|
3272
|
+
return p.charCodeAt(0) === 47;
|
|
3273
|
+
}
|
|
3274
|
+
function normalizeSegments(parts, allowAboveRoot) {
|
|
3275
|
+
const out = [];
|
|
3276
|
+
for (const part of parts) {
|
|
3277
|
+
if (part === "" || part === ".") continue;
|
|
3278
|
+
if (part === "..") {
|
|
3279
|
+
if (out.length > 0 && out[out.length - 1] !== "..") out.pop();
|
|
3280
|
+
else if (allowAboveRoot) out.push("..");
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
out.push(part);
|
|
3284
|
+
}
|
|
3285
|
+
return out;
|
|
3286
|
+
}
|
|
3287
|
+
function normalize(p) {
|
|
3288
|
+
if (p.length === 0) return ".";
|
|
3289
|
+
const absolute = isAbsolute(p);
|
|
3290
|
+
const trailingSlash = p.charCodeAt(p.length - 1) === 47;
|
|
3291
|
+
const segments = normalizeSegments(p.split("/"), !absolute);
|
|
3292
|
+
let joined = segments.join("/");
|
|
3293
|
+
if (joined.length === 0) {
|
|
3294
|
+
if (absolute) return "/";
|
|
3295
|
+
return trailingSlash ? "./" : ".";
|
|
3296
|
+
}
|
|
3297
|
+
if (trailingSlash) joined += "/";
|
|
3298
|
+
return absolute ? "/" + joined : joined;
|
|
3299
|
+
}
|
|
3300
|
+
function join2(...parts) {
|
|
3301
|
+
const present = parts.filter((part) => part.length > 0);
|
|
3302
|
+
if (present.length === 0) return ".";
|
|
3303
|
+
return normalize(present.join("/"));
|
|
3304
|
+
}
|
|
3305
|
+
function resolve2(...parts) {
|
|
3306
|
+
let resolved = "";
|
|
3307
|
+
let absolute = false;
|
|
3308
|
+
for (let i = parts.length - 1; i >= 0 && !absolute; i--) {
|
|
3309
|
+
const part = parts[i];
|
|
3310
|
+
if (part.length === 0) continue;
|
|
3311
|
+
resolved = resolved.length > 0 ? `${part}/${resolved}` : part;
|
|
3312
|
+
absolute = isAbsolute(part);
|
|
3313
|
+
}
|
|
3314
|
+
if (!absolute)
|
|
3315
|
+
throw new Error(
|
|
3316
|
+
`posixPath.resolve needs an absolute segment, got: ${parts.join(", ")}`
|
|
3317
|
+
);
|
|
3318
|
+
const normalized = normalizeSegments(resolved.split("/"), false).join("/");
|
|
3319
|
+
return normalized.length > 0 ? "/" + normalized : "/";
|
|
3320
|
+
}
|
|
3321
|
+
function dirname3(p) {
|
|
3322
|
+
const idx = p.lastIndexOf("/");
|
|
3323
|
+
if (idx < 0) return ".";
|
|
3324
|
+
if (idx === 0) return "/";
|
|
3325
|
+
return p.slice(0, idx);
|
|
3326
|
+
}
|
|
3327
|
+
function basename2(p, ext) {
|
|
3328
|
+
const idx = p.lastIndexOf("/");
|
|
3329
|
+
const base = idx < 0 ? p : p.slice(idx + 1);
|
|
3330
|
+
if (ext != null && ext.length < base.length && base.endsWith(ext)) {
|
|
3331
|
+
return base.slice(0, base.length - ext.length);
|
|
3332
|
+
}
|
|
3333
|
+
return base;
|
|
3334
|
+
}
|
|
3335
|
+
function extname(p) {
|
|
3336
|
+
const base = basename2(p);
|
|
3337
|
+
const idx = base.lastIndexOf(".");
|
|
3338
|
+
if (idx <= 0) return "";
|
|
3339
|
+
return base.slice(idx);
|
|
3340
|
+
}
|
|
3341
|
+
function relative(from, to) {
|
|
3342
|
+
if (from === to) return "";
|
|
3343
|
+
const fromParts = normalizeSegments(from.split("/"), false);
|
|
3344
|
+
const toParts = normalizeSegments(to.split("/"), false);
|
|
3345
|
+
let shared = 0;
|
|
3346
|
+
while (shared < fromParts.length && shared < toParts.length && fromParts[shared] === toParts[shared]) {
|
|
3347
|
+
shared++;
|
|
3348
|
+
}
|
|
3349
|
+
const up = new Array(fromParts.length - shared).fill("..");
|
|
3350
|
+
return [...up, ...toParts.slice(shared)].join("/");
|
|
3351
|
+
}
|
|
3352
|
+
var posix = {
|
|
3353
|
+
sep,
|
|
3354
|
+
delimiter,
|
|
3355
|
+
isAbsolute,
|
|
3356
|
+
normalize,
|
|
3357
|
+
join: join2,
|
|
3358
|
+
resolve: resolve2,
|
|
3359
|
+
dirname: dirname3,
|
|
3360
|
+
basename: basename2,
|
|
3361
|
+
extname,
|
|
3362
|
+
relative
|
|
3363
|
+
};
|
|
3364
|
+
var posixPath_default = { ...posix, posix };
|
|
3365
|
+
|
|
3366
|
+
// ../contrast-bundler/src/replacementModule.ts
|
|
3367
|
+
var REPLACEMENT_MODULE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
|
|
3368
|
+
var IMPORT_KEYWORD = import_types.TokenType["_import"];
|
|
3369
|
+
var EXPORT_KEYWORD = import_types.TokenType["_export"];
|
|
3370
|
+
var REACT_BRIDGE = "globalThis.__sootsim_react_bridge";
|
|
3371
|
+
var REACT_NATIVE_BRIDGE = "globalThis.__sootsim_components__";
|
|
3372
|
+
var PACKAGE_BRIDGES = "globalThis.__sootsim_config_package_modules";
|
|
3373
|
+
var JSX_RUNTIME_SPECIFIERS = /* @__PURE__ */ new Set(["react/jsx-runtime", "react/jsx-dev-runtime"]);
|
|
3374
|
+
function transformsFor(filePath) {
|
|
3375
|
+
switch (extname(filePath).toLowerCase()) {
|
|
3376
|
+
case ".ts":
|
|
3377
|
+
return ["typescript"];
|
|
3378
|
+
case ".tsx":
|
|
3379
|
+
return ["typescript", "jsx"];
|
|
3380
|
+
case ".js":
|
|
3381
|
+
case ".jsx":
|
|
3382
|
+
return ["jsx"];
|
|
3383
|
+
default:
|
|
3384
|
+
throw new Error(
|
|
3385
|
+
`replacement files must be ${REPLACEMENT_MODULE_EXTENSIONS.join(", ")}`
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
function isContrastKitSpecifier(specifier) {
|
|
3390
|
+
return specifier === "contrast-kit" || specifier.startsWith("contrast-kit/");
|
|
3391
|
+
}
|
|
3392
|
+
function unsupportedSpecifier(specifier) {
|
|
3393
|
+
return new Error(
|
|
3394
|
+
`replacement modules may only import react, react-native, contrast-kit, and the packages rnx stubs for this app; '${specifier}' is none of those. vendor the code into the replacement file, or add the package to rnx.config so the simulator bridges it.`
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
function bridgeDeclaration(name2, specifier) {
|
|
3398
|
+
const notReady = (what) => JSON.stringify(`[rnx] replacement ${what} bridge is not ready`);
|
|
3399
|
+
if (specifier === "react") {
|
|
3400
|
+
return `const ${name2} = ${REACT_BRIDGE}; if (!${name2}) throw new Error(${notReady("React")});`;
|
|
3401
|
+
}
|
|
3402
|
+
if (specifier === "react-native") {
|
|
3403
|
+
return `const ${name2} = ${REACT_NATIVE_BRIDGE}; if (!${name2}) throw new Error(${notReady("React Native")});`;
|
|
3404
|
+
}
|
|
3405
|
+
if (isContrastKitSpecifier(specifier)) {
|
|
3406
|
+
return `const ${name2} = (globalThis.__contrast_kit_modules && globalThis.__contrast_kit_modules[${JSON.stringify(specifier)}]) || (${PACKAGE_BRIDGES} || {})[${JSON.stringify(specifier)}]; if (!${name2}) throw new Error(${JSON.stringify(`[rnx] replacement package bridge ${specifier} is not ready`)});`;
|
|
3407
|
+
}
|
|
3408
|
+
if (JSX_RUNTIME_SPECIFIERS.has(specifier)) {
|
|
3409
|
+
return `const ${name2} = (() => { const react = ${REACT_BRIDGE}; if (!react) throw new Error(${notReady("React")}); const jsx = (type, props, key) => react.createElement(type, key === undefined ? props : { ...props, key }); return { Fragment: react.Fragment, jsx, jsxs: jsx, jsxDEV: jsx }; })();`;
|
|
3410
|
+
}
|
|
3411
|
+
return `const ${name2} = (${PACKAGE_BRIDGES} || {})[${JSON.stringify(specifier)}]; if (!${name2}) throw new Error(${JSON.stringify(`[rnx] replacement package bridge ${specifier} is not ready`)});`;
|
|
3412
|
+
}
|
|
3413
|
+
var DEFAULT_INTEROP = "__rnxReplacementDefault";
|
|
3414
|
+
var DEFAULT_INTEROP_DECLARATION = `const ${DEFAULT_INTEROP} = (bridge) => (bridge && bridge.__esModule ? bridge.default : bridge);`;
|
|
3415
|
+
function compileReplacementModule(input) {
|
|
3416
|
+
const transformed = (0, import_dist.transform)(input.source, {
|
|
3417
|
+
transforms: transformsFor(input.filePath),
|
|
3418
|
+
filePath: input.filePath,
|
|
3419
|
+
// classic runtime, matching what the app's own Metro build does for a file
|
|
3420
|
+
// that has no automatic-runtime pragma: the file imports React itself.
|
|
3421
|
+
jsxRuntime: "classic",
|
|
3422
|
+
jsxPragma: "React.createElement",
|
|
3423
|
+
jsxFragmentPragma: "React.Fragment",
|
|
3424
|
+
production: true,
|
|
3425
|
+
sourceMapOptions: { compiledFilename: input.filePath }
|
|
3426
|
+
});
|
|
3427
|
+
const code = transformed.code;
|
|
3428
|
+
const sourceMap = transformed.sourceMap;
|
|
3429
|
+
if (!sourceMap) throw new Error("sucrase compiled the replacement with no source map");
|
|
3430
|
+
const { tokens } = (0, import_parser.parse)(code, false, false, false);
|
|
3431
|
+
const read = (token) => code.slice(token.start, token.end);
|
|
3432
|
+
const readSpecifier = (token) => {
|
|
3433
|
+
if (token.type !== import_types.TokenType.string) {
|
|
3434
|
+
throw new Error("replacement module import has no string specifier");
|
|
3435
|
+
}
|
|
3436
|
+
return code.slice(token.start + 1, token.end - 1);
|
|
3437
|
+
};
|
|
3438
|
+
const bridgeNames = /* @__PURE__ */ new Map();
|
|
3439
|
+
const declarations = [];
|
|
3440
|
+
let needsDefaultInterop = false;
|
|
3441
|
+
const bridgeFor = (specifier) => {
|
|
3442
|
+
const existing = bridgeNames.get(specifier);
|
|
3443
|
+
if (existing) return existing;
|
|
3444
|
+
const name2 = `__rnxReplacementBridge${bridgeNames.size}`;
|
|
3445
|
+
bridgeNames.set(specifier, name2);
|
|
3446
|
+
declarations.push(bridgeDeclaration(name2, specifier));
|
|
3447
|
+
return name2;
|
|
3448
|
+
};
|
|
3449
|
+
const edits = [];
|
|
3450
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
3451
|
+
const token = tokens[index];
|
|
3452
|
+
if (token.type === EXPORT_KEYWORD) {
|
|
3453
|
+
const after2 = tokens[index + 1];
|
|
3454
|
+
let fromAt = -1;
|
|
3455
|
+
if (after2?.type === import_types.TokenType.star) {
|
|
3456
|
+
fromAt = read(tokens[index + 2]) === "from" ? index + 2 : index + 4;
|
|
3457
|
+
} else if (after2?.type === import_types.TokenType.braceL) {
|
|
3458
|
+
let depth = 1;
|
|
3459
|
+
let cursor2 = index + 2;
|
|
3460
|
+
while (cursor2 < tokens.length && depth > 0) {
|
|
3461
|
+
if (tokens[cursor2].type === import_types.TokenType.braceL) depth++;
|
|
3462
|
+
else if (tokens[cursor2].type === import_types.TokenType.braceR) depth--;
|
|
3463
|
+
cursor2++;
|
|
3464
|
+
}
|
|
3465
|
+
fromAt = cursor2;
|
|
3466
|
+
}
|
|
3467
|
+
if (fromAt > 0 && tokens[fromAt] && read(tokens[fromAt]) === "from" && tokens[fromAt + 1]?.type === import_types.TokenType.string) {
|
|
3468
|
+
const specifier2 = readSpecifier(tokens[fromAt + 1]);
|
|
3469
|
+
throw new Error(
|
|
3470
|
+
`a replacement module cannot re-export from another module, and this one re-exports from '${specifier2}'. import what you need and export the binding.`
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3473
|
+
continue;
|
|
3474
|
+
}
|
|
3475
|
+
if (token.type !== IMPORT_KEYWORD) continue;
|
|
3476
|
+
const after = tokens[index + 1];
|
|
3477
|
+
if (!after || after.type === import_types.TokenType.parenL || after.type === import_types.TokenType.dot) continue;
|
|
3478
|
+
const bindings = [];
|
|
3479
|
+
let cursor = index + 1;
|
|
3480
|
+
if (after.type !== import_types.TokenType.string) {
|
|
3481
|
+
while (cursor < tokens.length) {
|
|
3482
|
+
const clause = tokens[cursor];
|
|
3483
|
+
if (clause.type === import_types.TokenType.name && read(clause) === "from") break;
|
|
3484
|
+
if (clause.type === import_types.TokenType.comma) {
|
|
3485
|
+
cursor++;
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3488
|
+
if (clause.type === import_types.TokenType.star) {
|
|
3489
|
+
bindings.push({ local: read(tokens[cursor + 2]), imported: null });
|
|
3490
|
+
cursor += 3;
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
if (clause.type === import_types.TokenType.braceL) {
|
|
3494
|
+
cursor++;
|
|
3495
|
+
while (cursor < tokens.length && tokens[cursor].type !== import_types.TokenType.braceR) {
|
|
3496
|
+
if (tokens[cursor].type === import_types.TokenType.comma) {
|
|
3497
|
+
cursor++;
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
const nameToken = tokens[cursor];
|
|
3501
|
+
const imported = nameToken.type === import_types.TokenType.string ? readSpecifier(nameToken) : read(nameToken);
|
|
3502
|
+
let local = imported;
|
|
3503
|
+
if (tokens[cursor + 1] && read(tokens[cursor + 1]) === "as") {
|
|
3504
|
+
local = read(tokens[cursor + 2]);
|
|
3505
|
+
cursor += 2;
|
|
3506
|
+
}
|
|
3507
|
+
bindings.push({ local, imported });
|
|
3508
|
+
cursor++;
|
|
3509
|
+
}
|
|
3510
|
+
cursor++;
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
bindings.push({ local: read(clause), imported: "default" });
|
|
3514
|
+
cursor++;
|
|
3515
|
+
}
|
|
3516
|
+
cursor++;
|
|
3517
|
+
}
|
|
3518
|
+
const specifierToken = tokens[cursor];
|
|
3519
|
+
if (!specifierToken) throw new Error("replacement module import is unterminated");
|
|
3520
|
+
const specifier = readSpecifier(specifierToken);
|
|
3521
|
+
if (specifier !== "react" && specifier !== "react-native" && !JSX_RUNTIME_SPECIFIERS.has(specifier) && !isContrastKitSpecifier(specifier) && !input.runtimePackages.has(specifier)) {
|
|
3522
|
+
throw unsupportedSpecifier(specifier);
|
|
3523
|
+
}
|
|
3524
|
+
const bridge = bindings.length > 0 ? bridgeFor(specifier) : null;
|
|
3525
|
+
const replacement = bindings.map((binding) => {
|
|
3526
|
+
if (binding.imported === null) return `const ${binding.local} = ${bridge};`;
|
|
3527
|
+
if (binding.imported === "default") {
|
|
3528
|
+
needsDefaultInterop = true;
|
|
3529
|
+
return `const ${binding.local} = ${DEFAULT_INTEROP}(${bridge});`;
|
|
3530
|
+
}
|
|
3531
|
+
return `const ${binding.local} = ${bridge}[${JSON.stringify(binding.imported)}];`;
|
|
3532
|
+
}).join(" ");
|
|
3533
|
+
const start = token.start;
|
|
3534
|
+
const end = specifierToken.end;
|
|
3535
|
+
const newlines = code.slice(start, end).split("\n").length - 1;
|
|
3536
|
+
edits.push({ start, end, text: replacement + "\n".repeat(newlines) });
|
|
3537
|
+
}
|
|
3538
|
+
let out = code;
|
|
3539
|
+
for (const edit of edits.reverse()) {
|
|
3540
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
3541
|
+
}
|
|
3542
|
+
if (needsDefaultInterop) declarations.unshift(DEFAULT_INTEROP_DECLARATION);
|
|
3543
|
+
const preamble = declarations.join(" ");
|
|
3544
|
+
return {
|
|
3545
|
+
code: preamble ? `${preamble} ${out}` : out,
|
|
3546
|
+
sourceMap: { ...sourceMap, sourcesContent: [input.source] }
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// src/host/replacement-module-handler.ts
|
|
3551
|
+
var REPLACEMENT_MODULE_ROUTE = "/__sootsim-replacement-module";
|
|
3552
|
+
function sendJavaScript(req, res, source) {
|
|
3553
|
+
const origin = req.headers.origin;
|
|
3554
|
+
let allowedOrigin;
|
|
3555
|
+
if (origin) {
|
|
3556
|
+
try {
|
|
3557
|
+
if (isLoopbackHost(new URL(origin).hostname)) allowedOrigin = origin;
|
|
3558
|
+
} catch {
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
res.writeHead(200, {
|
|
3562
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
3563
|
+
"Cache-Control": "no-cache",
|
|
3564
|
+
...allowedOrigin ? { "Access-Control-Allow-Origin": allowedOrigin, Vary: "Origin" } : {}
|
|
3565
|
+
});
|
|
3566
|
+
res.end(req.method === "HEAD" ? void 0 : source);
|
|
3567
|
+
}
|
|
3568
|
+
function isReplacementModuleRequestUrl(rawUrl) {
|
|
3569
|
+
if (!rawUrl) return false;
|
|
3570
|
+
try {
|
|
3571
|
+
const pathname = new URL(rawUrl, "http://rnx.local").pathname;
|
|
3572
|
+
return pathname === REPLACEMENT_MODULE_ROUTE;
|
|
3573
|
+
} catch {
|
|
3574
|
+
return false;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
async function handleReplacementModuleRequest(req, res, options = {}) {
|
|
3578
|
+
const rawUrl = req.url || "";
|
|
3579
|
+
let url;
|
|
3580
|
+
try {
|
|
3581
|
+
url = new URL(rawUrl, "http://rnx.local");
|
|
3582
|
+
} catch {
|
|
3583
|
+
return false;
|
|
3584
|
+
}
|
|
3585
|
+
if (options.requireLoopbackOrigin) {
|
|
3586
|
+
const origin = req.headers.origin;
|
|
3587
|
+
const remoteAddress = req.socket.remoteAddress?.replace(/^::ffff:/, "");
|
|
3588
|
+
let allowed = Boolean(remoteAddress && isLoopbackHost(remoteAddress));
|
|
3589
|
+
if (allowed && origin) {
|
|
3590
|
+
try {
|
|
3591
|
+
allowed = isLoopbackHost(new URL(origin).hostname);
|
|
3592
|
+
} catch {
|
|
3593
|
+
allowed = false;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
if (!allowed) {
|
|
3597
|
+
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3598
|
+
res.end("replacement modules are available only to loopback origins");
|
|
3599
|
+
return true;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (url.pathname !== REPLACEMENT_MODULE_ROUTE) return false;
|
|
3603
|
+
if (options.requestToken && url.searchParams.get("token") !== options.requestToken) {
|
|
3604
|
+
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3605
|
+
res.end("replacement module request token is invalid");
|
|
3606
|
+
return true;
|
|
3607
|
+
}
|
|
3608
|
+
const targetPath = url.searchParams.get("path");
|
|
3609
|
+
if (!targetPath) {
|
|
3610
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3611
|
+
res.end("missing path param");
|
|
3612
|
+
return true;
|
|
3613
|
+
}
|
|
3614
|
+
const extension = import_node_path5.default.extname(targetPath).toLowerCase();
|
|
3615
|
+
if (!REPLACEMENT_MODULE_EXTENSIONS.some((allowed) => allowed === extension)) {
|
|
3616
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3617
|
+
res.end(`replacement files must be ${REPLACEMENT_MODULE_EXTENSIONS.join(", ")}`);
|
|
3618
|
+
return true;
|
|
3619
|
+
}
|
|
3620
|
+
if (!import_node_path5.default.isAbsolute(targetPath)) {
|
|
3621
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3622
|
+
res.end("replacement file path must be absolute");
|
|
3623
|
+
return true;
|
|
3624
|
+
}
|
|
3625
|
+
const runtimePackages = new Set(url.searchParams.getAll("runtimePackage"));
|
|
3626
|
+
let resolvedProjectRoot;
|
|
3627
|
+
if (options.projectRoot) {
|
|
3628
|
+
const lexicalProjectRoot = import_node_path5.default.resolve(options.projectRoot);
|
|
3629
|
+
resolvedProjectRoot = await import_promises.default.realpath(options.projectRoot);
|
|
3630
|
+
const lexicalTarget = import_node_path5.default.relative(lexicalProjectRoot, targetPath);
|
|
3631
|
+
if (lexicalTarget.startsWith("..") || import_node_path5.default.isAbsolute(lexicalTarget)) {
|
|
3632
|
+
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3633
|
+
res.end("replacement file must be inside the project root");
|
|
3634
|
+
return true;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
let resolvedTargetPath;
|
|
3638
|
+
try {
|
|
3639
|
+
resolvedTargetPath = await import_promises.default.realpath(targetPath);
|
|
3640
|
+
} catch (error) {
|
|
3641
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3642
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3643
|
+
res.end(`replacement file not found: ${message}`);
|
|
3644
|
+
return true;
|
|
3645
|
+
}
|
|
3646
|
+
if (resolvedProjectRoot) {
|
|
3647
|
+
const relativeTarget = import_node_path5.default.relative(resolvedProjectRoot, resolvedTargetPath);
|
|
3648
|
+
if (relativeTarget.startsWith("..") || import_node_path5.default.isAbsolute(relativeTarget)) {
|
|
3649
|
+
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3650
|
+
res.end("replacement file must be inside the project root");
|
|
3651
|
+
return true;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
try {
|
|
3655
|
+
const compiled = compileReplacementModule({
|
|
3656
|
+
filePath: resolvedTargetPath,
|
|
3657
|
+
source: await import_promises.default.readFile(resolvedTargetPath, "utf8"),
|
|
3658
|
+
runtimePackages
|
|
3659
|
+
});
|
|
3660
|
+
const map = Buffer.from(JSON.stringify(compiled.sourceMap), "utf8").toString("base64");
|
|
3661
|
+
sendJavaScript(
|
|
3662
|
+
req,
|
|
3663
|
+
res,
|
|
3664
|
+
`${compiled.code}
|
|
3665
|
+
//# sourceMappingURL=data:application/json;base64,${map}
|
|
3666
|
+
`
|
|
3667
|
+
);
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3670
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3671
|
+
res.end(`replacement compile failed: ${message}`);
|
|
3672
|
+
}
|
|
3673
|
+
return true;
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// src/runtime-assets.ts
|
|
3677
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
3678
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
3679
|
+
var import_url = require("url");
|
|
3680
|
+
|
|
3681
|
+
// ../contrast-runtime-delivery/src/product.ts
|
|
3682
|
+
function isValidRuntimeVersion(value) {
|
|
3683
|
+
return value.length <= 100 && /^[0-9A-Za-z][0-9A-Za-z._+-]*$/.test(value);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
// src/runtime-assets.ts
|
|
3687
|
+
var SOOTSIM_RUNTIME_MISSING_MESSAGE = "[rnx] no engine runtime installed. run `rnx runtime install`";
|
|
3688
|
+
var _monorepoRootCache;
|
|
3689
|
+
function findMonorepoRoot() {
|
|
3690
|
+
if (_monorepoRootCache !== void 0) return _monorepoRootCache;
|
|
3691
|
+
let dir = import_path2.default.dirname((0, import_url.fileURLToPath)(__sootsim_import_meta_url));
|
|
3692
|
+
for (let i = 0; i < 12 && dir !== import_path2.default.dirname(dir); i++) {
|
|
3693
|
+
if (import_fs3.default.existsSync(import_path2.default.join(dir, "packages/sootsim-engine/package.json")) && import_fs3.default.existsSync(import_path2.default.join(dir, "public/sootsim"))) {
|
|
3694
|
+
_monorepoRootCache = dir;
|
|
3695
|
+
return dir;
|
|
3696
|
+
}
|
|
3697
|
+
dir = import_path2.default.dirname(dir);
|
|
3698
|
+
}
|
|
3699
|
+
_monorepoRootCache = null;
|
|
3700
|
+
return null;
|
|
3701
|
+
}
|
|
3702
|
+
function resolveDevCheckoutRuntimeRoot() {
|
|
3703
|
+
if (process.env.RNX_HOME && readActiveRuntime()) return null;
|
|
3704
|
+
const root = findMonorepoRoot();
|
|
3705
|
+
if (!root) return null;
|
|
3706
|
+
const devBuild = import_path2.default.join(root, "public/sootsim");
|
|
3707
|
+
if (import_fs3.default.existsSync(import_path2.default.join(devBuild, "index.html")) && import_fs3.default.existsSync(import_path2.default.join(devBuild, "engine/index.html")) && import_fs3.default.existsSync(import_path2.default.join(devBuild, "engine-tenant/worker.js"))) {
|
|
3708
|
+
return devBuild;
|
|
3709
|
+
}
|
|
3710
|
+
return null;
|
|
3711
|
+
}
|
|
3712
|
+
var MIME_TYPES = {
|
|
3713
|
+
".js": "application/javascript",
|
|
3714
|
+
".mjs": "application/javascript",
|
|
3715
|
+
".css": "text/css",
|
|
3716
|
+
".html": "text/html",
|
|
3717
|
+
".wasm": "application/wasm",
|
|
3718
|
+
".json": "application/json",
|
|
3719
|
+
".jpg": "image/jpeg",
|
|
3720
|
+
".jpeg": "image/jpeg",
|
|
3721
|
+
".png": "image/png",
|
|
3722
|
+
".svg": "image/svg+xml",
|
|
3723
|
+
".webp": "image/webp",
|
|
3724
|
+
".glb": "model/gltf-binary",
|
|
3725
|
+
".ttf": "font/ttf",
|
|
3726
|
+
".otf": "font/otf",
|
|
3727
|
+
".woff": "font/woff",
|
|
3728
|
+
".woff2": "font/woff2",
|
|
3729
|
+
".mp3": "audio/mpeg",
|
|
3730
|
+
".wav": "audio/wav"
|
|
3731
|
+
};
|
|
3732
|
+
var ROOT_RUNTIME_PATHS = [
|
|
3733
|
+
"/assets/",
|
|
3734
|
+
"/engine/",
|
|
3735
|
+
"/engine-tenant/",
|
|
3736
|
+
"/photos/",
|
|
3737
|
+
"/three-mode/",
|
|
3738
|
+
"/fonts/",
|
|
3739
|
+
"/icons/",
|
|
3740
|
+
"/sounds/",
|
|
3741
|
+
"/spike/",
|
|
3742
|
+
"/test-wallpaper.jpg",
|
|
3743
|
+
"/preview-sw.js",
|
|
3744
|
+
"/sootsim.svg"
|
|
3745
|
+
];
|
|
3746
|
+
function resolveActiveRuntimeRoot() {
|
|
3747
|
+
const devRoot = resolveDevCheckoutRuntimeRoot();
|
|
3748
|
+
if (devRoot) return devRoot;
|
|
3749
|
+
const active = readActiveRuntime();
|
|
3750
|
+
if (!active) return null;
|
|
3751
|
+
const dir = runtimeDir(active);
|
|
3752
|
+
if (!import_fs3.default.existsSync(import_path2.default.join(dir, "index.html"))) return null;
|
|
3753
|
+
return dir;
|
|
3754
|
+
}
|
|
3755
|
+
var RUNTIME_VERSION_HOSTNAME_SUFFIX = ".runtime.localhost";
|
|
3756
|
+
function isRuntimeVersionHostname(hostname) {
|
|
3757
|
+
return hostname.toLowerCase().endsWith(RUNTIME_VERSION_HOSTNAME_SUFFIX);
|
|
3758
|
+
}
|
|
3759
|
+
function runtimeVersionFromHostname(hostname) {
|
|
3760
|
+
if (!isRuntimeVersionHostname(hostname)) return null;
|
|
3761
|
+
const labels = hostname.slice(0, -RUNTIME_VERSION_HOSTNAME_SUFFIX.length).split(".");
|
|
3762
|
+
if (labels.length === 0 || labels.some((label) => !/^[0-9a-f]{1,60}$/i.test(label))) {
|
|
3763
|
+
return null;
|
|
3764
|
+
}
|
|
3765
|
+
const encoded = labels.join("");
|
|
3766
|
+
if (encoded.length % 2 !== 0) return null;
|
|
3767
|
+
try {
|
|
3768
|
+
const version = Buffer.from(encoded, "hex").toString("utf8");
|
|
3769
|
+
return isValidRuntimeVersion(version) ? version : null;
|
|
3770
|
+
} catch {
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
function resolveInstalledRuntimeRoot(version) {
|
|
3775
|
+
const dir = runtimeDir(version);
|
|
3776
|
+
if (!import_fs3.default.existsSync(import_path2.default.join(dir, "index.html"))) return null;
|
|
3777
|
+
return dir;
|
|
3778
|
+
}
|
|
3779
|
+
function resolveRuntimeRootForHostname(hostname) {
|
|
3780
|
+
if (!isRuntimeVersionHostname(hostname)) return resolveActiveRuntimeRoot();
|
|
3781
|
+
const version = runtimeVersionFromHostname(hostname);
|
|
3782
|
+
return version ? resolveInstalledRuntimeRoot(version) : null;
|
|
3783
|
+
}
|
|
3784
|
+
function resolveRuntimeRootForRequestHost(host) {
|
|
3785
|
+
if (!host) return resolveActiveRuntimeRoot();
|
|
3786
|
+
try {
|
|
3787
|
+
return resolveRuntimeRootForHostname(new URL(`http://${host}`).hostname);
|
|
3788
|
+
} catch {
|
|
3789
|
+
return null;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
function isRootRuntimeAssetPath(pathname) {
|
|
3793
|
+
return ROOT_RUNTIME_PATHS.some((p) => pathname === p || pathname.startsWith(p));
|
|
3794
|
+
}
|
|
3795
|
+
function resolveRuntimeFilePath(runtimeRoot, pathname) {
|
|
3796
|
+
if (!pathname.startsWith("/")) return null;
|
|
3797
|
+
if (pathname.includes("\0") || pathname.includes("\\")) return null;
|
|
3798
|
+
for (const segment of pathname.split("/")) {
|
|
3799
|
+
if (segment === "..") return null;
|
|
3800
|
+
}
|
|
3801
|
+
const fullPath = import_path2.default.resolve(runtimeRoot, pathname.replace(/^\/+/, ""));
|
|
3802
|
+
const rootWithSep = runtimeRoot.endsWith(import_path2.default.sep) ? runtimeRoot : runtimeRoot + import_path2.default.sep;
|
|
3803
|
+
if (!fullPath.startsWith(rootWithSep) && fullPath !== runtimeRoot) return null;
|
|
3804
|
+
if (!import_fs3.default.existsSync(fullPath) || !import_fs3.default.statSync(fullPath).isFile()) return null;
|
|
3805
|
+
return fullPath;
|
|
3806
|
+
}
|
|
3807
|
+
function serveRuntimeFile(req, res, fullPath) {
|
|
3808
|
+
const ext = import_path2.default.extname(fullPath);
|
|
3809
|
+
const stat = import_fs3.default.statSync(fullPath);
|
|
3810
|
+
const lastModified = stat.mtime.toUTCString();
|
|
3811
|
+
res.setHeader("content-type", MIME_TYPES[ext] || "application/octet-stream");
|
|
3812
|
+
if (/^canvaskit-graphite-pipelines-[0-9a-f]{8}\.bin$/.test(import_path2.default.basename(fullPath))) {
|
|
3813
|
+
res.setHeader("cache-control", "public, max-age=31536000, immutable");
|
|
3814
|
+
import_fs3.default.createReadStream(fullPath).pipe(res);
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
res.setHeader("cache-control", "no-cache");
|
|
3818
|
+
res.setHeader("last-modified", lastModified);
|
|
3819
|
+
if (req?.headers?.["if-modified-since"] === lastModified) {
|
|
3820
|
+
res.statusCode = 304;
|
|
3821
|
+
res.end();
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
import_fs3.default.createReadStream(fullPath).pipe(res);
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
// src/vite-plugin-swift.ts
|
|
3828
|
+
var import_node_child_process2 = require("node:child_process");
|
|
3829
|
+
var import_node_os2 = __toESM(require("node:os"), 1);
|
|
3830
|
+
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
3831
|
+
var import_node_util = require("node:util");
|
|
3832
|
+
|
|
3833
|
+
// ../sootsim-swift/toolchain.json
|
|
3834
|
+
var toolchain_default = {
|
|
3835
|
+
release: "swift-6.3-RELEASE",
|
|
3836
|
+
wasmSdkChecksum: "9fa4016ee632c7e9e906608ec3b55cf13dfc4dff44e47574c5af58064dc33fd9"
|
|
3837
|
+
};
|
|
3838
|
+
|
|
3839
|
+
// src/vite-plugin-swift.ts
|
|
3840
|
+
var run = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
3841
|
+
var TOOLCHAIN_BIN = import_node_path6.default.join(
|
|
3842
|
+
import_node_os2.default.homedir(),
|
|
3843
|
+
`Library/Developer/Toolchains/${toolchain_default.release}.xctoolchain/usr/bin`
|
|
3844
|
+
);
|
|
3845
|
+
var WASM_SDK = `${toolchain_default.release}_wasm`;
|
|
3846
|
+
var BUNDLE_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
3847
|
+
var BUNDLE_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
3848
|
+
|
|
3849
|
+
// src/vite-plugin-one.ts
|
|
3850
|
+
function rnxPlugin(options = {}) {
|
|
3851
|
+
const prefix2 = options.prefix || "/__soot";
|
|
3852
|
+
return {
|
|
3853
|
+
name: "sootsim-one",
|
|
3854
|
+
// use-latest-callback ships a CJS main (module.exports, no default) with
|
|
3855
|
+
// an esm.mjs shim that re-imports it. react-native-bottom-tabs
|
|
3856
|
+
// default-imports it from source served live, so vite hands the browser
|
|
3857
|
+
// the raw CJS file and the import fails with 'does not provide an export
|
|
3858
|
+
// named default' before anything mounts. forcing it through dep
|
|
3859
|
+
// optimization lets esbuild's CJS interop answer the default import, and
|
|
3860
|
+
// every bare import of it (including the nested copy's) resolves to the
|
|
3861
|
+
// one pre-bundled instance.
|
|
3862
|
+
config() {
|
|
3863
|
+
return { optimizeDeps: { include: ["use-latest-callback"] } };
|
|
3864
|
+
},
|
|
3865
|
+
configureServer(server) {
|
|
3866
|
+
if (options.enabled === false) return;
|
|
3867
|
+
const bundleUrl = options.bundleUrl || "/node_modules/one/metro-entry.bundle?platform=ios&dev=true&minify=false";
|
|
3868
|
+
const appName = resolveRnxDesktopAppName(options.open, server.config.root);
|
|
3869
|
+
const replacementToken = (0, import_node_crypto4.randomUUID)();
|
|
3870
|
+
const configuredPort = server.config.server.port || 8081;
|
|
3871
|
+
const listeningPort = () => {
|
|
3872
|
+
const address = server.httpServer?.address();
|
|
3873
|
+
return address && typeof address === "object" ? address.port : configuredPort;
|
|
3874
|
+
};
|
|
3875
|
+
const bundleForPort = (port2) => tagRnxBundleForPlugin(
|
|
3876
|
+
bundleUrl,
|
|
3877
|
+
`http://localhost:${port2}`,
|
|
3878
|
+
replacementToken,
|
|
3879
|
+
server.config.root,
|
|
3880
|
+
appName
|
|
3881
|
+
);
|
|
3882
|
+
const buildShellHtml = (runtimeRoot) => {
|
|
3883
|
+
const html = import_fs4.default.readFileSync(import_path3.default.join(runtimeRoot, "index.html"), "utf8");
|
|
3884
|
+
const lock = readDaemonLockfile();
|
|
3885
|
+
const bridgePort = lock && isDaemonLockfileFresh(lock) && lock.bridgePort > 0 ? lock.bridgePort : DEFAULT_SOOTSIM_BRIDGE_PORT;
|
|
3886
|
+
const inject = `<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleForPort(listeningPort()))});window.__sootsimBridgePort=${bridgePort};history.replaceState(null,'','${prefix2}/'+u.search)})()</script>`;
|
|
3887
|
+
return html.replace("</head>", inject + "</head>");
|
|
3888
|
+
};
|
|
3889
|
+
server.middlewares.use((req, res, next) => {
|
|
3890
|
+
const origWriteHead = res.writeHead.bind(res);
|
|
3891
|
+
res.writeHead = function(statusCode, ...args) {
|
|
3892
|
+
const setCookie = res.getHeader("set-cookie");
|
|
3893
|
+
if (setCookie) {
|
|
3894
|
+
const value = Array.isArray(setCookie) ? setCookie.join(", ") : String(setCookie);
|
|
3895
|
+
res.setHeader("x-sootsim-set-cookie", value);
|
|
3896
|
+
res.setHeader("access-control-expose-headers", "x-sootsim-set-cookie");
|
|
3897
|
+
}
|
|
3898
|
+
return origWriteHead(
|
|
3899
|
+
statusCode,
|
|
3900
|
+
...args
|
|
3901
|
+
);
|
|
3902
|
+
};
|
|
3903
|
+
next();
|
|
3904
|
+
});
|
|
3905
|
+
server.middlewares.use((req, res, next) => {
|
|
3906
|
+
if ((req.url || "").split("?")[0] !== "/__server-scan") return next();
|
|
3907
|
+
const scanPort = listeningPort();
|
|
3908
|
+
const pluginBundleUrl = bundleForPort(scanPort);
|
|
3909
|
+
const absoluteBundleUrl = pluginBundleUrl.startsWith("http") ? pluginBundleUrl : `http://localhost:${scanPort}${pluginBundleUrl}`;
|
|
3910
|
+
res.setHeader("content-type", "application/json");
|
|
3911
|
+
res.end(
|
|
3912
|
+
JSON.stringify([
|
|
3913
|
+
{
|
|
3914
|
+
port: scanPort,
|
|
3915
|
+
framework: "one",
|
|
3916
|
+
bundleUrl: absoluteBundleUrl,
|
|
3917
|
+
...appName ? { projectName: appName } : {},
|
|
3918
|
+
lastSeen: Date.now()
|
|
3919
|
+
}
|
|
3920
|
+
])
|
|
3921
|
+
);
|
|
3922
|
+
});
|
|
3923
|
+
server.middlewares.use((req, res, next) => {
|
|
3924
|
+
void handleReplacementModuleRequest(req, res, {
|
|
3925
|
+
projectRoot: server.config.root,
|
|
3926
|
+
requestToken: replacementToken,
|
|
3927
|
+
requireLoopbackOrigin: true
|
|
3928
|
+
}).then((handled) => {
|
|
3929
|
+
if (!handled) next();
|
|
3930
|
+
});
|
|
3931
|
+
});
|
|
3932
|
+
server.middlewares.use((req, res, next) => {
|
|
3933
|
+
const url = req.url || "";
|
|
3934
|
+
const pathname = url.split("?")[0];
|
|
3935
|
+
const runtimeRoot = resolveRuntimeRootForRequestHost(req.headers.host);
|
|
3936
|
+
if (!runtimeRoot) {
|
|
3937
|
+
if (pathname === prefix2 || pathname === prefix2 + "/" || pathname.startsWith(prefix2 + "/")) {
|
|
3938
|
+
res.statusCode = 500;
|
|
3939
|
+
res.end(SOOTSIM_RUNTIME_MISSING_MESSAGE);
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
next();
|
|
3943
|
+
return;
|
|
3944
|
+
}
|
|
3945
|
+
if (isRootRuntimeAssetPath(pathname)) {
|
|
3946
|
+
const fullPath = resolveRuntimeFilePath(runtimeRoot, pathname);
|
|
3947
|
+
if (fullPath) {
|
|
3948
|
+
serveRuntimeFile(req, res, fullPath);
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
if (pathname === prefix2 || pathname === prefix2 + "/") {
|
|
3953
|
+
res.setHeader("content-type", "text/html");
|
|
3954
|
+
res.end(buildShellHtml(runtimeRoot));
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
if (pathname.startsWith(prefix2 + "/")) {
|
|
3958
|
+
const fullPath = resolveRuntimeFilePath(
|
|
3959
|
+
runtimeRoot,
|
|
3960
|
+
pathname.slice(prefix2.length)
|
|
3961
|
+
);
|
|
3962
|
+
if (fullPath) {
|
|
3963
|
+
serveRuntimeFile(req, res, fullPath);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
const ext = import_path3.default.extname(pathname);
|
|
3967
|
+
if (!ext) {
|
|
3968
|
+
res.setHeader("content-type", "text/html");
|
|
3969
|
+
res.end(buildShellHtml(runtimeRoot));
|
|
3970
|
+
return;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
next();
|
|
3974
|
+
});
|
|
3975
|
+
const port = configuredPort;
|
|
3976
|
+
const sootsimUrl = `http://localhost:${port}${prefix2}/`;
|
|
3977
|
+
console.log(`[rnx] serving at ${sootsimUrl}`);
|
|
3978
|
+
if (appName && !process.env.CI && !process.env.RNX_NO_OPEN) {
|
|
3979
|
+
server.httpServer?.once("listening", () => {
|
|
3980
|
+
const openPort = listeningPort();
|
|
3981
|
+
launchRnxDesktop(
|
|
3982
|
+
resolveRnxDesktopLaunchTarget(openPort, bundleForPort(openPort)),
|
|
3983
|
+
appName
|
|
3984
|
+
);
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
};
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
// src/metro-plugin.ts
|
|
3992
|
+
var import_fs5 = __toESM(require("fs"), 1);
|
|
3993
|
+
var import_module2 = require("module");
|
|
3994
|
+
var import_node_crypto5 = require("node:crypto");
|
|
3995
|
+
var import_path4 = __toESM(require("path"), 1);
|
|
3996
|
+
var prefix = "/__soot";
|
|
3997
|
+
function readCallableMetroExport(moduleValue, label) {
|
|
3998
|
+
const callable = typeof moduleValue === "function" ? moduleValue : typeof moduleValue === "object" && moduleValue !== null ? Reflect.get(moduleValue, "default") : void 0;
|
|
3999
|
+
if (typeof callable !== "function") {
|
|
4000
|
+
throw new Error(`rnxsim: Metro ${label} module did not export a function`);
|
|
4001
|
+
}
|
|
4002
|
+
return (...args) => Reflect.apply(callable, void 0, args);
|
|
4003
|
+
}
|
|
4004
|
+
function createMetroDefaultSerializer(projectRoot) {
|
|
4005
|
+
const projectRequire = (0, import_module2.createRequire)(import_path4.default.join(projectRoot, "package.json"));
|
|
4006
|
+
const metroRoot = import_path4.default.dirname(projectRequire.resolve("metro/package.json"));
|
|
4007
|
+
const baseJSBundle = readCallableMetroExport(
|
|
4008
|
+
projectRequire(import_path4.default.join(metroRoot, "src/DeltaBundler/Serializers/baseJSBundle.js")),
|
|
4009
|
+
"baseJSBundle"
|
|
4010
|
+
);
|
|
4011
|
+
const bundleToString = readCallableMetroExport(
|
|
4012
|
+
projectRequire(import_path4.default.join(metroRoot, "src/lib/bundleToString.js")),
|
|
4013
|
+
"bundleToString"
|
|
4014
|
+
);
|
|
4015
|
+
return (entryPoint, preModules, graph, options) => {
|
|
4016
|
+
const serialized = bundleToString(
|
|
4017
|
+
baseJSBundle(entryPoint, preModules, graph, options)
|
|
4018
|
+
);
|
|
4019
|
+
const code = typeof serialized === "object" && serialized !== null ? Reflect.get(serialized, "code") : void 0;
|
|
4020
|
+
if (typeof code !== "string") {
|
|
4021
|
+
throw new Error("rnxsim: Metro bundleToString did not return bundle code");
|
|
4022
|
+
}
|
|
4023
|
+
return code;
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
function appendRNXModulePaths(result, graph, options, projectRoot) {
|
|
4027
|
+
const dependencies = graph.dependencies;
|
|
4028
|
+
const createModuleId = options.createModuleId;
|
|
4029
|
+
if (!dependencies || !createModuleId) {
|
|
4030
|
+
return result;
|
|
4031
|
+
}
|
|
4032
|
+
const modulePaths = {};
|
|
4033
|
+
for (const module2 of dependencies.values()) {
|
|
4034
|
+
modulePaths[String(createModuleId(module2.path))] = import_path4.default.relative(projectRoot, module2.path).replaceAll(import_path4.default.sep, "/");
|
|
4035
|
+
}
|
|
4036
|
+
if (Object.keys(modulePaths).length !== dependencies.size) {
|
|
4037
|
+
throw new Error("rnxsim: bundle module IDs were not unique");
|
|
4038
|
+
}
|
|
4039
|
+
const specifiersByModuleId = /* @__PURE__ */ new Map();
|
|
4040
|
+
for (const module2 of dependencies.values()) {
|
|
4041
|
+
for (const [dependencyKey, dependency] of module2.dependencies ?? []) {
|
|
4042
|
+
const specifier = dependency.data?.name ?? dependencyKey;
|
|
4043
|
+
if (specifier.length === 0 || specifier.startsWith(".") || specifier.startsWith("/") || typeof dependency.absolutePath !== "string") {
|
|
4044
|
+
continue;
|
|
4045
|
+
}
|
|
4046
|
+
const moduleId = String(createModuleId(dependency.absolutePath));
|
|
4047
|
+
if (!Object.prototype.hasOwnProperty.call(modulePaths, moduleId)) continue;
|
|
4048
|
+
const found = specifiersByModuleId.get(moduleId);
|
|
4049
|
+
if (found) found.add(specifier);
|
|
4050
|
+
else specifiersByModuleId.set(moduleId, /* @__PURE__ */ new Set([specifier]));
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const logicalSpecifiers = Object.fromEntries(
|
|
4054
|
+
[...specifiersByModuleId].sort(([left], [right]) => Number(left) - Number(right)).map(([moduleId, specifiers]) => [moduleId, [...specifiers].sort()])
|
|
4055
|
+
);
|
|
4056
|
+
const identity = {
|
|
4057
|
+
version: RNX_METRO_MODULE_IDENTITY_VERSION,
|
|
4058
|
+
identitySource: "rnx-plugin",
|
|
4059
|
+
modulePaths,
|
|
4060
|
+
logicalSpecifiers
|
|
4061
|
+
};
|
|
4062
|
+
return typeof result === "string" ? appendRNXMetroModuleIdentityFooter(result, identity) : { ...result, code: appendRNXMetroModuleIdentityFooter(result.code, identity) };
|
|
4063
|
+
}
|
|
4064
|
+
function withRNX(config) {
|
|
4065
|
+
const cfgProjectRoot = config.projectRoot || process.cwd();
|
|
4066
|
+
const configuredBundleUrl = "/index.bundle?platform=ios&dev=true&hot=true&minify=false";
|
|
4067
|
+
const cfgServer = config.server;
|
|
4068
|
+
const port = resolveMetroPort(cfgServer);
|
|
4069
|
+
const metroServerId = (0, import_node_crypto5.randomUUID)();
|
|
4070
|
+
const bundleUrl = tagRnxBundleForPlugin(
|
|
4071
|
+
configuredBundleUrl,
|
|
4072
|
+
`http://localhost:${port}`,
|
|
4073
|
+
metroServerId,
|
|
4074
|
+
cfgProjectRoot
|
|
4075
|
+
);
|
|
4076
|
+
const cfgSerializer = config.serializer;
|
|
4077
|
+
const existingEnhance = cfgServer?.enhanceMiddleware;
|
|
4078
|
+
const existingSerializer = cfgSerializer?.customSerializer;
|
|
4079
|
+
let defaultSerializer;
|
|
4080
|
+
const serializerConfig = {
|
|
4081
|
+
serializer: {
|
|
4082
|
+
...cfgSerializer,
|
|
4083
|
+
customSerializer: async (entryPoint, preModules, graph, serializerOptions) => {
|
|
4084
|
+
const serializer = existingSerializer || (defaultSerializer ||= createMetroDefaultSerializer(cfgProjectRoot));
|
|
4085
|
+
const result = await serializer(entryPoint, preModules, graph, serializerOptions);
|
|
4086
|
+
return appendRNXModulePaths(result, graph, serializerOptions, cfgProjectRoot);
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
};
|
|
4090
|
+
const serverConfig = {
|
|
4091
|
+
...cfgServer,
|
|
4092
|
+
enhanceMiddleware: (metroMiddleware, metroServer) => {
|
|
4093
|
+
let middleware = metroMiddleware;
|
|
4094
|
+
if (existingEnhance) {
|
|
4095
|
+
middleware = existingEnhance(metroMiddleware, metroServer);
|
|
4096
|
+
}
|
|
4097
|
+
let connect2;
|
|
4098
|
+
try {
|
|
4099
|
+
connect2 = require("connect");
|
|
4100
|
+
} catch {
|
|
4101
|
+
console.warn("[rnx] connect not available");
|
|
4102
|
+
return middleware;
|
|
4103
|
+
}
|
|
4104
|
+
const server = connect2();
|
|
4105
|
+
server.use((req, res, next) => {
|
|
4106
|
+
if (isReplacementModuleRequestUrl(req.url)) {
|
|
4107
|
+
void handleReplacementModuleRequest(req, res, {
|
|
4108
|
+
projectRoot: cfgProjectRoot,
|
|
4109
|
+
requestToken: metroServerId,
|
|
4110
|
+
requireLoopbackOrigin: true
|
|
4111
|
+
}).then((handled) => {
|
|
4112
|
+
if (!handled) next();
|
|
4113
|
+
});
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
const url = req.url || "";
|
|
4117
|
+
const pathname = url.split("?")[0];
|
|
4118
|
+
const runtimeRoot = resolveRuntimeRootForRequestHost(req.headers?.host);
|
|
4119
|
+
if (!runtimeRoot) {
|
|
4120
|
+
if (pathname === prefix || pathname === prefix + "/") {
|
|
4121
|
+
res.statusCode = 500;
|
|
4122
|
+
res.end(SOOTSIM_RUNTIME_MISSING_MESSAGE);
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
next();
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
4128
|
+
if (isRootRuntimeAssetPath(pathname)) {
|
|
4129
|
+
const fullPath = resolveRuntimeFilePath(runtimeRoot, pathname);
|
|
4130
|
+
if (fullPath) {
|
|
4131
|
+
serveRuntimeFile(req, res, fullPath);
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
if (pathname.startsWith(prefix + "/")) {
|
|
4136
|
+
const fullPath = resolveRuntimeFilePath(
|
|
4137
|
+
runtimeRoot,
|
|
4138
|
+
pathname.slice(prefix.length)
|
|
4139
|
+
);
|
|
4140
|
+
if (fullPath) {
|
|
4141
|
+
serveRuntimeFile(req, res, fullPath);
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
if (pathname === prefix || pathname === prefix + "/" || pathname.startsWith(prefix + "/")) {
|
|
4146
|
+
const ext = import_path4.default.extname(pathname);
|
|
4147
|
+
if (!ext) {
|
|
4148
|
+
const htmlPath = import_path4.default.join(runtimeRoot, "index.html");
|
|
4149
|
+
let html = import_fs5.default.readFileSync(htmlPath, "utf8");
|
|
4150
|
+
const script = `<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleUrl)});history.replaceState(null,'','${prefix}/'+u.search)})()</script>`;
|
|
4151
|
+
html = html.includes("</head>") ? html.replace("</head>", `${script}</head>`) : `${html}${script}`;
|
|
4152
|
+
res.setHeader("content-type", "text/html");
|
|
4153
|
+
res.end(html);
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
next();
|
|
4158
|
+
});
|
|
4159
|
+
server.use(middleware);
|
|
4160
|
+
console.log(`[rnx] serving at ${prefix}/`);
|
|
4161
|
+
return server;
|
|
4162
|
+
}
|
|
4163
|
+
};
|
|
4164
|
+
return {
|
|
4165
|
+
...config,
|
|
4166
|
+
...serializerConfig,
|
|
4167
|
+
server: serverConfig
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
function resolveMetroPort(server) {
|
|
4171
|
+
const inlinePort = process.argv.find((arg) => arg.startsWith("--port="))?.slice(7);
|
|
4172
|
+
const portIndex = process.argv.indexOf("--port");
|
|
4173
|
+
const splitPort = portIndex >= 0 ? process.argv[portIndex + 1] : void 0;
|
|
4174
|
+
const rawPort = inlinePort || splitPort || process.env.RCT_METRO_PORT || server?.port || 8081;
|
|
4175
|
+
const port = Number(rawPort);
|
|
4176
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
4177
|
+
throw new Error(`rnx: Metro port must be a positive integer, got ${rawPort}`);
|
|
4178
|
+
}
|
|
4179
|
+
return port;
|
|
4180
|
+
}
|
|
4181
|
+
var metro_plugin_default = withRNX;
|
|
4182
|
+
|
|
4183
|
+
// src/jump-to-source-babel.ts
|
|
4184
|
+
function normalizeSourcePath(path9) {
|
|
4185
|
+
const queryStart = path9.indexOf("?");
|
|
4186
|
+
const clean = queryStart >= 0 ? path9.slice(0, queryStart) : path9;
|
|
4187
|
+
return clean.replace(/\\/g, "/");
|
|
4188
|
+
}
|
|
4189
|
+
function matchesPath(path9, matcher) {
|
|
4190
|
+
return typeof matcher === "string" ? path9.includes(matcher) : matcher.test(path9);
|
|
4191
|
+
}
|
|
4192
|
+
function shouldStampFile(filename, options) {
|
|
4193
|
+
const normalized = normalizeSourcePath(filename);
|
|
4194
|
+
if (normalized.includes("/node_modules/")) return false;
|
|
4195
|
+
if (normalized.includes("/dist/") || normalized.includes("/build/")) return false;
|
|
4196
|
+
if (options.exclude?.some((matcher) => matchesPath(normalized, matcher))) return false;
|
|
4197
|
+
if (options.include?.length) {
|
|
4198
|
+
return options.include.some((matcher) => matchesPath(normalized, matcher));
|
|
4199
|
+
}
|
|
4200
|
+
return true;
|
|
4201
|
+
}
|
|
4202
|
+
function getJSXName(node) {
|
|
4203
|
+
if (!node) return null;
|
|
4204
|
+
if (node.type === "JSXIdentifier" && node.name) return node.name;
|
|
4205
|
+
if (node.type === "JSXMemberExpression") {
|
|
4206
|
+
const object = getJSXName(node.object);
|
|
4207
|
+
const property = getJSXName(node.property);
|
|
4208
|
+
return object && property ? `${object}.${property}` : null;
|
|
4209
|
+
}
|
|
4210
|
+
return null;
|
|
4211
|
+
}
|
|
4212
|
+
function hasAttribute(node, attrName) {
|
|
4213
|
+
return node.attributes?.some((attr) => {
|
|
4214
|
+
if (attr.type !== "JSXAttribute") return false;
|
|
4215
|
+
return attr.name?.type === "JSXIdentifier" && attr.name.name === attrName;
|
|
4216
|
+
}) ?? false;
|
|
4217
|
+
}
|
|
4218
|
+
function sootsimJumpToSourceBabelPlugin(api, options = {}) {
|
|
4219
|
+
const attrName = options.attrName || "srcloc";
|
|
4220
|
+
return {
|
|
4221
|
+
name: "sootsim-jump-to-source",
|
|
4222
|
+
visitor: {
|
|
4223
|
+
JSXOpeningElement(path9, state) {
|
|
4224
|
+
if (options.enabled === false) return;
|
|
4225
|
+
const filename = state.file?.opts?.filename;
|
|
4226
|
+
if (!filename || !shouldStampFile(filename, options)) return;
|
|
4227
|
+
const node = path9.node;
|
|
4228
|
+
const tagName = getJSXName(node.name);
|
|
4229
|
+
if (!tagName || tagName === "Fragment" || tagName.endsWith(".Fragment")) return;
|
|
4230
|
+
if (hasAttribute(node, attrName)) return;
|
|
4231
|
+
const loc = node.loc?.start;
|
|
4232
|
+
if (!loc) return;
|
|
4233
|
+
const source = `${normalizeSourcePath(filename)}:${loc.line}:${loc.column + 1}`;
|
|
4234
|
+
node.attributes ??= [];
|
|
4235
|
+
node.attributes.push(
|
|
4236
|
+
api.types.jsxAttribute(
|
|
4237
|
+
api.types.jsxIdentifier(attrName),
|
|
4238
|
+
api.types.stringLiteral(source)
|
|
4239
|
+
)
|
|
4240
|
+
);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4246
|
+
0 && (module.exports = {
|
|
4247
|
+
DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
4248
|
+
DEFAULT_SOOTSIM_BRIDGE_URL,
|
|
4249
|
+
DEFAULT_SOOTSIM_SHELL_URL,
|
|
4250
|
+
RNX_SCREEN_CAPTURE_VERSION,
|
|
4251
|
+
SimClient,
|
|
4252
|
+
SimClientError,
|
|
4253
|
+
SimConnectError,
|
|
4254
|
+
SimKeyboardError,
|
|
4255
|
+
SimPerformError,
|
|
4256
|
+
SimReadyTimeoutError,
|
|
4257
|
+
SimResetError,
|
|
4258
|
+
SimTapError,
|
|
4259
|
+
connect,
|
|
4260
|
+
isRnxCaptureRegionAtlas,
|
|
4261
|
+
isRnxScreenCapture,
|
|
4262
|
+
jumpToSourceBabelPlugin,
|
|
4263
|
+
metroPlugin,
|
|
4264
|
+
rnxCaptureRasterRegions,
|
|
4265
|
+
rnxPlugin,
|
|
4266
|
+
rnxRasterFrames,
|
|
4267
|
+
selectRnxRasterNodeIds
|
|
4268
|
+
});
|