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,346 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildLoopbackProbeBaseUrl,
|
|
3
|
+
isLoopbackHost,
|
|
4
|
+
LOOPBACK_PROBE_HOSTNAMES,
|
|
5
|
+
} from './backend-origin'
|
|
6
|
+
import {
|
|
7
|
+
alignLoopbackBundleUrlWithProbe,
|
|
8
|
+
METRO_BUNDLE_PATH,
|
|
9
|
+
normalizeNativeDevBundleUrl,
|
|
10
|
+
} from './native-dev-bundle-url'
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_TEST_DRIVE_PORT = 8081
|
|
13
|
+
|
|
14
|
+
export interface ResolvedDevBundle {
|
|
15
|
+
bundleUrl: string
|
|
16
|
+
port: number
|
|
17
|
+
framework: string
|
|
18
|
+
projectName?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildUnresolvedTargetError(targetLabel: string): Error {
|
|
22
|
+
return new Error(
|
|
23
|
+
`could not resolve a native bundle for ${targetLabel}. pass an explicit bundle URL or open Connect and choose the app there.`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function inferManifestFramework(
|
|
28
|
+
launchUrl: string | undefined,
|
|
29
|
+
sdkVersion: unknown,
|
|
30
|
+
): string {
|
|
31
|
+
if (launchUrl?.includes('/one/metro-entry.bundle')) return 'one'
|
|
32
|
+
if (typeof sdkVersion === 'string' && sdkVersion) return 'expo'
|
|
33
|
+
return 'unknown'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildBaseUrl(protocol: string, host: string, port: number) {
|
|
37
|
+
return `${protocol}//${host}:${port}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function shouldProxyLoopbackProbe(targetUrl: string) {
|
|
41
|
+
if (typeof window === 'undefined') return false
|
|
42
|
+
try {
|
|
43
|
+
const parsed = new URL(targetUrl)
|
|
44
|
+
return (
|
|
45
|
+
isLoopbackHost(parsed.hostname) &&
|
|
46
|
+
isLoopbackHost(window.location.hostname) &&
|
|
47
|
+
parsed.origin !== window.location.origin
|
|
48
|
+
)
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fetchDevProbe(targetUrl: string, init?: RequestInit): Promise<Response> {
|
|
55
|
+
const requestInit: RequestInit = {
|
|
56
|
+
...init,
|
|
57
|
+
cache: init?.cache ?? 'no-store',
|
|
58
|
+
}
|
|
59
|
+
if (!shouldProxyLoopbackProbe(targetUrl)) {
|
|
60
|
+
return fetch(targetUrl, requestInit)
|
|
61
|
+
}
|
|
62
|
+
return fetch(`/__fetch-proxy?url=${encodeURIComponent(targetUrl)}`, requestInit)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getDefaultPortForProtocol(protocol: string) {
|
|
66
|
+
return protocol === 'https:' ? 443 : 80
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isBaseServerUrl(url: URL) {
|
|
70
|
+
const path = url.pathname || '/'
|
|
71
|
+
return (path === '/' || path === '') && !url.search && !url.hash
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// resolve a loopback dev-server port through the SAME server-side scanner the
|
|
75
|
+
// dev-server list uses (`/__server-scan`), so an explicit port (`/rn/<port>`, a
|
|
76
|
+
// typed port) gets the same manifest path and query the list resolves. loopback
|
|
77
|
+
// hosts stay aligned to the address that answered discovery.
|
|
78
|
+
//
|
|
79
|
+
// why this exists: in a browser/worker the direct manifest probe below is
|
|
80
|
+
// cross-origin (shell on :5173, dev server on :<port>), and a dev server
|
|
81
|
+
// answers a cross-origin `GET /` with its SSR HTML, not the expo manifest. so
|
|
82
|
+
// the manifest branch silently fails (`.json()` throws on HTML) and the probe
|
|
83
|
+
// falls back to a generic `/index.bundle` — a DIFFERENT metro entry point than
|
|
84
|
+
// the manifest's `metro-entry.bundle`, which makes metro build the same app
|
|
85
|
+
// twice. the scanner runs server-side (same-origin node fetch, sends the
|
|
86
|
+
// expo-platform header) and reads `launchAsset.url` correctly.
|
|
87
|
+
//
|
|
88
|
+
// env-agnostic by design: it just attempts `/__server-scan` and lets the result
|
|
89
|
+
// decide. in the tenant worker (which has no `location`) the patched fetch
|
|
90
|
+
// resolves the relative path to the shell origin and returns the scan json. in a
|
|
91
|
+
// node/bun CLI a relative-URL fetch with no base throws → null → the direct probe
|
|
92
|
+
// runs (correct there, since node can read the cross-origin manifest). in prod
|
|
93
|
+
// the path SPA-falls-back to HTML → the json content-type guard returns null →
|
|
94
|
+
// direct probe. so there is exactly one resolver per environment, no env sniff.
|
|
95
|
+
async function resolvePortViaServerScan(port: number): Promise<ResolvedDevBundle | null> {
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch('/__server-scan', { cache: 'no-store' })
|
|
98
|
+
if (!res.ok) return null
|
|
99
|
+
if (!(res.headers.get('content-type') || '').toLowerCase().includes('json'))
|
|
100
|
+
return null
|
|
101
|
+
const servers: unknown = await res.json()
|
|
102
|
+
if (!Array.isArray(servers)) return null
|
|
103
|
+
const match = servers.find(
|
|
104
|
+
(
|
|
105
|
+
s,
|
|
106
|
+
): s is {
|
|
107
|
+
port: number
|
|
108
|
+
bundleUrl: string
|
|
109
|
+
framework?: string
|
|
110
|
+
projectName?: string
|
|
111
|
+
bundleUrlProvisional?: boolean
|
|
112
|
+
} =>
|
|
113
|
+
!!s &&
|
|
114
|
+
typeof s === 'object' &&
|
|
115
|
+
(s as { port?: unknown }).port === port &&
|
|
116
|
+
typeof (s as { bundleUrl?: unknown }).bundleUrl === 'string',
|
|
117
|
+
)
|
|
118
|
+
if (!match) return null
|
|
119
|
+
// the scanner marks a bundle URL provisional when its own fast manifest
|
|
120
|
+
// probe got no answer and it fell back to the generic `/index.bundle`. that
|
|
121
|
+
// is enough to LIST the server and not enough to open it, so fall through
|
|
122
|
+
// to the direct probe below, which asks the manifest again with a budget
|
|
123
|
+
// that fits a real dev server. same rule the CLI applies in
|
|
124
|
+
// `resolveConnectionInputForCli`; a provisional URL must never reach a sim.
|
|
125
|
+
if (match.bundleUrlProvisional) return null
|
|
126
|
+
return {
|
|
127
|
+
// normalize exactly like the manifest-probe branch below — the scan
|
|
128
|
+
// returns metro's manifest launchAsset url (lazy=true for code-split
|
|
129
|
+
// one/vxrn apps), but sootsim loads ONE self-contained bundle into the tenant
|
|
130
|
+
// worker, so a deferred React.lazy chunk throws "Requiring unknown module
|
|
131
|
+
// <id>". normalizeNativeDevBundleUrl forces lazy=false + strips bytecode.
|
|
132
|
+
bundleUrl: normalizeNativeDevBundleUrl(match.bundleUrl),
|
|
133
|
+
port,
|
|
134
|
+
framework: typeof match.framework === 'string' ? match.framework : 'unknown',
|
|
135
|
+
projectName: typeof match.projectName === 'string' ? match.projectName : undefined,
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// how a dev server answered `GET /`, which is the one request that tells the
|
|
143
|
+
// two kinds of dev server apart. an Expo or One server answers with its
|
|
144
|
+
// manifest. a vanilla Metro server answers with Metro's own HTML landing page
|
|
145
|
+
// and has no manifest to serve. NO answer is neither: a One server that is
|
|
146
|
+
// still starting leaves `/` hanging until its manifest middleware is up, while
|
|
147
|
+
// `/status` already reports packager-status:running. reading that silence as
|
|
148
|
+
// "this server has no manifest" opens the app on `<base>/index.bundle` — a
|
|
149
|
+
// second entry point, with its own module graph, that the app's manifest never
|
|
150
|
+
// named — so Metro builds the whole app twice and the connection is labelled
|
|
151
|
+
// `metro` instead of its real framework.
|
|
152
|
+
type ManifestProbe =
|
|
153
|
+
| { state: 'resolved'; bundle: ResolvedDevBundle }
|
|
154
|
+
| { state: 'no-manifest' }
|
|
155
|
+
| { state: 'no-answer' }
|
|
156
|
+
|
|
157
|
+
// a `/` that has not answered in this long is a server still coming up, not a
|
|
158
|
+
// server telling us what it is. without it an unresponsive dev server hangs
|
|
159
|
+
// the caller for as long as it feels like.
|
|
160
|
+
const MANIFEST_PROBE_TIMEOUT_MS = 5_000
|
|
161
|
+
// how long to keep asking a confirmed-running packager for its manifest.
|
|
162
|
+
const MANIFEST_BOOT_BUDGET_MS = 30_000
|
|
163
|
+
const MANIFEST_BOOT_RETRY_MS = 1_000
|
|
164
|
+
|
|
165
|
+
async function probeExpoManifest(
|
|
166
|
+
normalizedBaseUrl: string,
|
|
167
|
+
port: number,
|
|
168
|
+
): Promise<ManifestProbe> {
|
|
169
|
+
let body: string
|
|
170
|
+
// the deadline means STOP WAITING, not KILL THE REQUEST. `AbortSignal.timeout`
|
|
171
|
+
// does the second: it tears down a dev server that is still assembling its
|
|
172
|
+
// manifest, which fires "Cannot pipe to a closed or destroyed stream" into
|
|
173
|
+
// One's manifest middleware and can exit the user's dev server. so nothing
|
|
174
|
+
// here aborts. we race the probe against the budget, answer `no-answer` on
|
|
175
|
+
// time, and let the request run itself out in the background, discarded.
|
|
176
|
+
const pending = fetchDevProbe(`${normalizedBaseUrl}/`, {
|
|
177
|
+
headers: { 'expo-platform': 'ios' },
|
|
178
|
+
}).then(
|
|
179
|
+
async (res) => (res.ok ? { ok: true as const, body: await res.text() } : null),
|
|
180
|
+
() => undefined,
|
|
181
|
+
)
|
|
182
|
+
let probeDeadline: ReturnType<typeof setTimeout> | undefined
|
|
183
|
+
const answered = await Promise.race([
|
|
184
|
+
pending,
|
|
185
|
+
new Promise<'timeout'>((resolve) => {
|
|
186
|
+
probeDeadline = setTimeout(() => resolve('timeout'), MANIFEST_PROBE_TIMEOUT_MS)
|
|
187
|
+
}),
|
|
188
|
+
])
|
|
189
|
+
// the race is settled, so drop the deadline rather than leaving it pending.
|
|
190
|
+
clearTimeout(probeDeadline)
|
|
191
|
+
if (answered === 'timeout' || answered === undefined) return { state: 'no-answer' }
|
|
192
|
+
if (answered === null) return { state: 'no-manifest' }
|
|
193
|
+
body = answered.body
|
|
194
|
+
if (!body.trim()) return { state: 'no-answer' }
|
|
195
|
+
|
|
196
|
+
let manifest: any
|
|
197
|
+
try {
|
|
198
|
+
manifest = JSON.parse(body)
|
|
199
|
+
} catch {
|
|
200
|
+
// a page rather than a manifest — Metro's landing page, or an app's own
|
|
201
|
+
// SSR html. that is a real answer: this server serves no manifest.
|
|
202
|
+
return { state: 'no-manifest' }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const client = manifest?.extra?.expoClient || manifest?.extra || {}
|
|
206
|
+
const rawLaunchUrl =
|
|
207
|
+
typeof manifest?.launchAsset?.url === 'string' ? manifest.launchAsset.url : undefined
|
|
208
|
+
const launchUrl = rawLaunchUrl
|
|
209
|
+
? alignLoopbackBundleUrlWithProbe(rawLaunchUrl, normalizedBaseUrl)
|
|
210
|
+
: undefined
|
|
211
|
+
if (!launchUrl && !client.name) return { state: 'no-manifest' }
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
state: 'resolved',
|
|
215
|
+
bundle: {
|
|
216
|
+
bundleUrl: normalizeNativeDevBundleUrl(
|
|
217
|
+
launchUrl || `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
|
|
218
|
+
),
|
|
219
|
+
port,
|
|
220
|
+
framework: inferManifestFramework(launchUrl, client.sdkVersion),
|
|
221
|
+
projectName: client.name,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function isPackagerRunning(normalizedBaseUrl: string): Promise<boolean> {
|
|
227
|
+
try {
|
|
228
|
+
const res = await fetchDevProbe(`${normalizedBaseUrl}/status`)
|
|
229
|
+
if (!res.ok) return false
|
|
230
|
+
return (await res.text()).includes('packager-status:running')
|
|
231
|
+
} catch {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function probeBaseUrlBundleDirect(
|
|
237
|
+
baseUrl: string,
|
|
238
|
+
port: number,
|
|
239
|
+
): Promise<ResolvedDevBundle | null> {
|
|
240
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
|
|
241
|
+
|
|
242
|
+
let manifest = await probeExpoManifest(normalizedBaseUrl, port)
|
|
243
|
+
let packagerConfirmed = false
|
|
244
|
+
|
|
245
|
+
// nothing answered `/`. that is either a closed address — the other loopback
|
|
246
|
+
// family, a dead port — or a packager whose manifest middleware has not come
|
|
247
|
+
// up yet. `/status` separates them, and only the second is worth waiting on.
|
|
248
|
+
if (manifest.state === 'no-answer') {
|
|
249
|
+
packagerConfirmed = await isPackagerRunning(normalizedBaseUrl)
|
|
250
|
+
if (!packagerConfirmed) return null
|
|
251
|
+
const deadline = Date.now() + MANIFEST_BOOT_BUDGET_MS
|
|
252
|
+
while (manifest.state === 'no-answer' && Date.now() < deadline) {
|
|
253
|
+
await new Promise((resolve) => setTimeout(resolve, MANIFEST_BOOT_RETRY_MS))
|
|
254
|
+
manifest = await probeExpoManifest(normalizedBaseUrl, port)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (manifest.state === 'resolved') return manifest.bundle
|
|
259
|
+
// a server that never says what it is gets no guessed entry point.
|
|
260
|
+
if (manifest.state === 'no-answer') return null
|
|
261
|
+
|
|
262
|
+
// vanilla Metro (or any non-Expo dev server) — packager-status:running on
|
|
263
|
+
// /status is the canonical signature. we previously also did `HEAD
|
|
264
|
+
// /node_modules/one/metro-entry.bundle` as a framework discriminator, but
|
|
265
|
+
// Metro lazy-resolves modules at request time, so HEAD on *any* `.bundle`
|
|
266
|
+
// path can return 200 even when the underlying file doesn't exist. that
|
|
267
|
+
// caused false-positive One framework detection on non-One apps that had
|
|
268
|
+
// `node_modules/one` anywhere on disk (or that returned 200 HEAD + 404
|
|
269
|
+
// GET for missing bundles), surfacing as
|
|
270
|
+
// "failed to fetch bundle (404|502): .../node_modules/one/metro-entry.bundle…"
|
|
271
|
+
// the Expo manifest probe above already returns the correct launchAsset.url
|
|
272
|
+
// for One apps (which serve the Expo manifest format via vxrn), so we
|
|
273
|
+
// drop the bespoke One probe entirely.
|
|
274
|
+
if (!packagerConfirmed && !(await isPackagerRunning(normalizedBaseUrl))) return null
|
|
275
|
+
return {
|
|
276
|
+
bundleUrl: `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
|
|
277
|
+
port,
|
|
278
|
+
framework: 'metro',
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function probeBaseUrlBundle(
|
|
283
|
+
baseUrl: string,
|
|
284
|
+
port: number,
|
|
285
|
+
): Promise<ResolvedDevBundle | null> {
|
|
286
|
+
// browser/worker loopback targets resolve through the server-side scanner so
|
|
287
|
+
// they match the dev-server list exactly — see resolvePortViaServerScan.
|
|
288
|
+
try {
|
|
289
|
+
if (isLoopbackHost(new URL(baseUrl).hostname)) {
|
|
290
|
+
const viaScan = await resolvePortViaServerScan(port)
|
|
291
|
+
if (viaScan) return viaScan
|
|
292
|
+
}
|
|
293
|
+
} catch {}
|
|
294
|
+
return probeBaseUrlBundleDirect(baseUrl, port)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function probePortBundle(port: number): Promise<ResolvedDevBundle | null> {
|
|
298
|
+
const viaScan = await resolvePortViaServerScan(port)
|
|
299
|
+
if (viaScan) return viaScan
|
|
300
|
+
const candidates = await Promise.all(
|
|
301
|
+
LOOPBACK_PROBE_HOSTNAMES.map((hostname) =>
|
|
302
|
+
probeBaseUrlBundleDirect(buildLoopbackProbeBaseUrl('http:', hostname, port), port),
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
return candidates.find((candidate) => candidate !== null) ?? null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function resolveConnectionInput(input: string): Promise<ResolvedDevBundle> {
|
|
309
|
+
const trimmed = input.trim()
|
|
310
|
+
|
|
311
|
+
if (/^\d+$/.test(trimmed)) {
|
|
312
|
+
const port = parseInt(trimmed, 10)
|
|
313
|
+
const resolved = await probePortBundle(port)
|
|
314
|
+
if (resolved) return resolved
|
|
315
|
+
throw buildUnresolvedTargetError(`localhost:${port}`)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const bundleUrl = trimmed.startsWith('http') ? trimmed : `http://${trimmed}`
|
|
319
|
+
let parsed: URL
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
parsed = new URL(bundleUrl)
|
|
323
|
+
} catch {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`could not parse "${input}". pass a dev-server port, a dev-server base URL, or a full bundle URL.`,
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const protocol = parsed.protocol || 'http:'
|
|
330
|
+
const port = parsed.port
|
|
331
|
+
? parseInt(parsed.port, 10)
|
|
332
|
+
: getDefaultPortForProtocol(protocol)
|
|
333
|
+
const baseUrl = buildBaseUrl(protocol, parsed.hostname, port)
|
|
334
|
+
|
|
335
|
+
// a bare base URL (just an origin, no path) is unambiguously a dev server
|
|
336
|
+
// to probe — regardless of host. probe its live manifest rather than
|
|
337
|
+
// guessing that the origin itself is the bundle URL. loopback vs. remote
|
|
338
|
+
// only affects how the probe fetch is routed (see shouldProxyLoopbackProbe).
|
|
339
|
+
if (isBaseServerUrl(parsed)) {
|
|
340
|
+
const resolved = await probeBaseUrlBundle(baseUrl, port)
|
|
341
|
+
if (resolved) return resolved
|
|
342
|
+
throw buildUnresolvedTargetError(baseUrl)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { bundleUrl: parsed.toString(), port, framework: 'unknown' }
|
|
346
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import http from 'node:http'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
export type RnxDesktopOpenOption = boolean | { appName: string }
|
|
9
|
+
|
|
10
|
+
const scheduledMetroOpens = new Set<string>()
|
|
11
|
+
|
|
12
|
+
export function resolveRnxDesktopAppName(
|
|
13
|
+
open: RnxDesktopOpenOption | undefined,
|
|
14
|
+
projectRoot: string,
|
|
15
|
+
): string | null {
|
|
16
|
+
if (!open) return null
|
|
17
|
+
if (typeof open === 'object') {
|
|
18
|
+
const appName = open.appName.trim()
|
|
19
|
+
if (!appName) throw new Error('rnx: open.appName must not be empty')
|
|
20
|
+
return appName
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const packageJsonPath = path.join(projectRoot, 'package.json')
|
|
24
|
+
const packageJson: unknown = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
25
|
+
const packageName =
|
|
26
|
+
packageJson && typeof packageJson === 'object'
|
|
27
|
+
? Reflect.get(packageJson, 'name')
|
|
28
|
+
: undefined
|
|
29
|
+
if (typeof packageName !== 'string' || !packageName.trim()) {
|
|
30
|
+
throw new Error(`rnx: ${packageJsonPath} must declare a package name`)
|
|
31
|
+
}
|
|
32
|
+
return packageName.trim()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function tagRnxBundleForPlugin(
|
|
36
|
+
bundleUrl: string,
|
|
37
|
+
replacementOrigin: string,
|
|
38
|
+
replacementToken: string,
|
|
39
|
+
projectRoot: string,
|
|
40
|
+
appName?: string | null,
|
|
41
|
+
): string {
|
|
42
|
+
const absolute = /^[a-z][a-z\d+.-]*:\/\//i.test(bundleUrl)
|
|
43
|
+
const url = new URL(bundleUrl, absolute ? undefined : 'http://rnx.local')
|
|
44
|
+
if (appName) {
|
|
45
|
+
url.searchParams.set('rnxAppName', appName)
|
|
46
|
+
url.searchParams.set(
|
|
47
|
+
'rnxProjectId',
|
|
48
|
+
createHash('sha256')
|
|
49
|
+
.update(fs.realpathSync(projectRoot))
|
|
50
|
+
.digest('hex')
|
|
51
|
+
.slice(0, 32),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
url.searchParams.set('rnxReplacementOrigin', replacementOrigin)
|
|
55
|
+
url.searchParams.set('rnxReplacementToken', replacementToken)
|
|
56
|
+
return absolute ? url.toString() : `${url.pathname}${url.search}${url.hash}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveRnxDesktopLaunchTarget(port: number, bundleUrl: string): string {
|
|
60
|
+
return new URL(bundleUrl, `http://localhost:${port}`).toString()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildRnxDesktopOpenArgs(bundleUrl: string): string[] {
|
|
64
|
+
return ['open', bundleUrl, '--new', '--driver', 'electron', '--no-describe']
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function observeRnxDesktopLaunch(child: ChildProcess, appName: string): void {
|
|
68
|
+
let stderr = ''
|
|
69
|
+
let spawnFailed = false
|
|
70
|
+
child.stderr?.setEncoding('utf8')
|
|
71
|
+
child.stderr?.on('data', (chunk: string) => {
|
|
72
|
+
stderr = `${stderr}${chunk}`.slice(-16_384)
|
|
73
|
+
})
|
|
74
|
+
child.once('error', (error) => {
|
|
75
|
+
spawnFailed = true
|
|
76
|
+
console.error(`[rnx] could not open ${appName}: ${error.message}`)
|
|
77
|
+
})
|
|
78
|
+
child.once('close', (code, signal) => {
|
|
79
|
+
if (spawnFailed) return
|
|
80
|
+
if (code === 0) {
|
|
81
|
+
console.log(`[rnx] opened ${appName} in the desktop simulator`)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const detail = stderr.trim() || (signal ? `signal ${signal}` : `exit ${code}`)
|
|
85
|
+
console.error(`[rnx] could not open ${appName}: ${detail}`)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function launchRnxDesktop(bundleUrl: string, appName: string): void {
|
|
90
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
91
|
+
const binPath = path.join(packageRoot, 'dist-cli', 'bin.js')
|
|
92
|
+
console.log(`[rnx] opening ${appName} in the desktop simulator`)
|
|
93
|
+
const child = spawn(
|
|
94
|
+
process.execPath,
|
|
95
|
+
[binPath, ...buildRnxDesktopOpenArgs(bundleUrl)],
|
|
96
|
+
{
|
|
97
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
observeRnxDesktopLaunch(child, appName)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface MetroDesktopOpenOptions {
|
|
105
|
+
port: number
|
|
106
|
+
bundleUrl: string
|
|
107
|
+
appName: string
|
|
108
|
+
projectRoot: string
|
|
109
|
+
serverId: string
|
|
110
|
+
intervalMs?: number
|
|
111
|
+
timeoutMs?: number
|
|
112
|
+
launch?: (bundleUrl: string, appName: string) => void
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function openRnxDesktopWhenMetroIsReady({
|
|
116
|
+
port,
|
|
117
|
+
bundleUrl,
|
|
118
|
+
appName,
|
|
119
|
+
projectRoot,
|
|
120
|
+
serverId,
|
|
121
|
+
intervalMs = 100,
|
|
122
|
+
timeoutMs = 30_000,
|
|
123
|
+
launch = launchRnxDesktop,
|
|
124
|
+
}: MetroDesktopOpenOptions): Promise<boolean> {
|
|
125
|
+
const key = `${projectRoot}\0${port}`
|
|
126
|
+
if (scheduledMetroOpens.has(key)) return Promise.resolve(false)
|
|
127
|
+
scheduledMetroOpens.add(key)
|
|
128
|
+
const deadline = Date.now() + timeoutMs
|
|
129
|
+
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
let finished = false
|
|
132
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined
|
|
133
|
+
const finish = (opened: boolean) => {
|
|
134
|
+
if (finished) return
|
|
135
|
+
finished = true
|
|
136
|
+
clearTimeout(retryTimer)
|
|
137
|
+
scheduledMetroOpens.delete(key)
|
|
138
|
+
resolve(opened)
|
|
139
|
+
}
|
|
140
|
+
const retry = () => {
|
|
141
|
+
if (finished) return
|
|
142
|
+
if (Date.now() >= deadline) {
|
|
143
|
+
console.error(
|
|
144
|
+
`[rnx] could not open ${appName}: Metro did not become ready on port ${port}`,
|
|
145
|
+
)
|
|
146
|
+
finish(false)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
retryTimer = setTimeout(check, intervalMs)
|
|
150
|
+
}
|
|
151
|
+
const check = () => {
|
|
152
|
+
const request = http.get(
|
|
153
|
+
`http://127.0.0.1:${port}/__server-scan`,
|
|
154
|
+
{ headers: { Accept: 'application/json' } },
|
|
155
|
+
(response) => {
|
|
156
|
+
let body = ''
|
|
157
|
+
response.setEncoding('utf8')
|
|
158
|
+
response.on('data', (chunk: string) => {
|
|
159
|
+
body = `${body}${chunk}`.slice(0, 65_536)
|
|
160
|
+
})
|
|
161
|
+
response.once('end', () => {
|
|
162
|
+
try {
|
|
163
|
+
const scan: unknown = JSON.parse(body)
|
|
164
|
+
const matches =
|
|
165
|
+
response.statusCode === 200 &&
|
|
166
|
+
Array.isArray(scan) &&
|
|
167
|
+
scan.some((entry: unknown) => {
|
|
168
|
+
if (!entry || typeof entry !== 'object') return false
|
|
169
|
+
const scannedPort = Reflect.get(entry, 'port')
|
|
170
|
+
const projectName = Reflect.get(entry, 'projectName')
|
|
171
|
+
const scannedServerId = Reflect.get(entry, 'rnxServerId')
|
|
172
|
+
const scannedBundle = Reflect.get(entry, 'bundleUrl')
|
|
173
|
+
if (
|
|
174
|
+
scannedPort !== port ||
|
|
175
|
+
projectName !== appName ||
|
|
176
|
+
scannedServerId !== serverId ||
|
|
177
|
+
typeof scannedBundle !== 'string'
|
|
178
|
+
) {
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
return new URL(scannedBundle).searchParams.get('rnxAppName') === appName
|
|
182
|
+
})
|
|
183
|
+
if (matches) {
|
|
184
|
+
launch(bundleUrl, appName)
|
|
185
|
+
finish(true)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
} catch {}
|
|
189
|
+
retry()
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
request.setTimeout(Math.min(1_000, timeoutMs), () => request.destroy())
|
|
194
|
+
request.once('error', retry)
|
|
195
|
+
}
|
|
196
|
+
check()
|
|
197
|
+
})
|
|
198
|
+
}
|