peachsim 0.1.629 → 0.1.631
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/cli/app-fonts.ts +23 -4
- package/cli/bridge-flow-runner.ts +45 -31
- package/cli/cli-update.ts +2 -7
- package/cli/cloud-test-tunnel.ts +16 -3
- package/cli/command-registry.ts +1 -0
- package/cli/commands/compat.ts +11 -60
- package/cli/commands/config.ts +1 -1
- package/cli/commands/control.ts +40 -8
- package/cli/commands/daemon-mac-app.ts +4 -2
- package/cli/commands/daemon.ts +4 -4
- package/cli/commands/detect.ts +289 -26
- package/cli/commands/device.ts +2 -3
- package/cli/commands/inspect/actions.ts +125 -90
- package/cli/commands/inspect/core.ts +1 -1
- package/cli/commands/inspect.ts +10 -1
- package/cli/commands/maestro.ts +3 -1
- package/cli/commands/platform.ts +1 -1
- package/cli/commands/project-report.ts +386 -0
- package/cli/commands/projectReportContract.ts +42 -0
- package/cli/commands/report-issue.ts +38 -54
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +6 -4
- package/cli/commands/skills.ts +0 -31
- package/cli/commands/telemetry.ts +8 -3
- package/cli/commands/test.ts +347 -134
- package/cli/drivers/playwright-provisioning.ts +3 -2
- package/cli/drivers/playwright-sim-host.ts +25 -9
- package/cli/main.ts +19 -3
- package/cli/outbound-endpoints.ts +2 -5
- package/cli/privacy.ts +1 -2
- package/cli/runtime-diagnostics.ts +51 -0
- package/cli/telemetry.ts +78 -7
- package/detox/index.ts +77 -51
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +130 -33
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +8 -5
- package/dist-lib/host/bridge-host.cjs +353 -290
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +257 -121
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
- package/dist-lib/menu.cjs +11 -1
- package/dist-lib/menu.mjs +11 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +132 -9
- package/dist-lib/native-app-identity.cjs +278 -0
- package/dist-lib/native-app-identity.mjs +245 -0
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +112 -87
- package/dist-lib/sdk.mjs +111 -87
- package/dist-lib/skills.cjs +1270 -709
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +672 -275
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +11 -28
- package/kotlin/README.md +32 -4
- package/kotlin/fixtures/counter/Counter.kt +26 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
- package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
- package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
- package/kotlin/fork/LICENSE +178 -0
- package/kotlin/fork/NOTICE +25 -0
- package/kotlin/fork/README.md +59 -0
- package/kotlin/fork/build.sh +128 -0
- package/kotlin/fork/compose-worker-globals.patch +96 -0
- package/kotlin/fork/fork.json +33 -0
- package/kotlin/fork/publish.ts +77 -0
- package/kotlin/fork/skiko-worker-clock.patch +33 -0
- package/kotlin/toolchain.json +288 -0
- package/package.json +13 -5
- package/skills/contrast/SKILL.md +1 -0
- package/src/automatic-update-policy.ts +17 -0
- package/src/home-paths.ts +11 -9
- package/src/host/bridge-host.ts +3 -1
- package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
- package/src/menu.ts +11 -0
- package/src/metro-plugin.ts +18 -0
- package/src/native-app-identity.ts +376 -0
- package/src/native-firebase-config.ts +90 -0
- package/src/screenshots/frame-compose.ts +1 -1
- package/src/screenshots/registry.ts +1 -1
- package/src/screenshots/schema.ts +2 -2
- package/src/vite-plugin-one.ts +11 -28
- package/src/vite-plugin.ts +6 -1
|
@@ -2,9 +2,9 @@ import { spawnSync } from 'node:child_process'
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
|
+
import { isRecord } from '../../src/is-record'
|
|
5
6
|
import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
|
|
6
7
|
import { rnxSelfInvocation } from '../self-invocation'
|
|
7
|
-
import { isRecord } from '../../src/is-record'
|
|
8
8
|
|
|
9
9
|
export interface ResolvedPlaywright {
|
|
10
10
|
// package name that resolved (for human-facing messaging)
|
|
@@ -32,7 +32,8 @@ export function resolvePlaywright(): ResolvedPlaywright | null {
|
|
|
32
32
|
// resolve from several roots, not just cwd. a user can invoke a global CLI
|
|
33
33
|
// from an arbitrary directory while Playwright is installed beside the CLI.
|
|
34
34
|
const roots = [`${process.cwd()}/`]
|
|
35
|
-
|
|
35
|
+
const self = rnxSelfInvocation()
|
|
36
|
+
roots.push(self.prefixArgs[0] ?? self.executable)
|
|
36
37
|
for (const root of roots) {
|
|
37
38
|
try {
|
|
38
39
|
const requireFromRoot = createRequire(root)
|
|
@@ -2,6 +2,11 @@ import {
|
|
|
2
2
|
readBrowserCompositeCaptureRequest,
|
|
3
3
|
resolveBrowserCompositeCdpClip,
|
|
4
4
|
} from '../../../peach-engine/src/capture/browser-composite'
|
|
5
|
+
import {
|
|
6
|
+
AUTH_BROWSER_EXTERNAL_NAVIGATION_EVENT,
|
|
7
|
+
AUTH_BROWSER_SESSION_FRAGMENT_KEY,
|
|
8
|
+
forwardPopupSchemeNavigations,
|
|
9
|
+
} from '../../../peach-engine/src/open-external'
|
|
5
10
|
import { createBrowserPointerDispatcher } from '../../../peach-engine/src/render-worker/browser-pointer'
|
|
6
11
|
// the detached browser host, as CJS source.
|
|
7
12
|
//
|
|
@@ -46,11 +51,13 @@ const cdpPort = process.env.SOOTSIM_PW_CDP_PORT || '';
|
|
|
46
51
|
const extraChromeArgs = (process.env.SOOTSIM_PW_CHROME_ARGS || '')
|
|
47
52
|
.split(' ')
|
|
48
53
|
.filter(Boolean);
|
|
49
|
-
//
|
|
50
|
-
// renderer
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
+
// A Mac always has a GPU, so a local macOS drive that lands on Playwright's
|
|
55
|
+
// software renderer is a misconfiguration that pegs every core: it fails
|
|
56
|
+
// closed unless the hardware-backed test browser is proven after launch. CI
|
|
57
|
+
// and Linux or Windows hosts, which are often GPU-less servers, use the
|
|
58
|
+
// bundled browser and render in software when no GPU is present.
|
|
59
|
+
const hardwareGpuRequired =
|
|
60
|
+
process.platform === 'darwin' && process.env.CI !== '1' && process.env.CI !== 'true';
|
|
54
61
|
const softwareRendererPattern = /swiftshader|software(?: rasterizer)?|llvmpipe|lavapipe|softpipe/i;
|
|
55
62
|
// page viewport as '<w>x<h>'. the caller sizes it from the device profile;
|
|
56
63
|
// the fallback is phone-shaped because the page hosts a phone sim — the
|
|
@@ -74,6 +81,7 @@ const { execSync } = require('child_process');
|
|
|
74
81
|
const { tmpdir } = require('os');
|
|
75
82
|
const { join } = require('path');
|
|
76
83
|
const createBrowserPointerDispatcher = ${createBrowserPointerDispatcher.toString()};
|
|
84
|
+
const forwardPopupSchemeNavigations = ${forwardPopupSchemeNavigations.toString()};
|
|
77
85
|
const readBrowserCompositeCaptureRequest = ${readBrowserCompositeCaptureRequest.toString()};
|
|
78
86
|
const resolveBrowserCompositeCdpClip = ${resolveBrowserCompositeCdpClip.toString()};
|
|
79
87
|
// chrome tree reaper — playwright's browser.close() and even SIGKILLing the
|
|
@@ -300,9 +308,9 @@ const cleanupProfile = () => {
|
|
|
300
308
|
startOwnerWatch();
|
|
301
309
|
// Local rendering uses Playwright's full Chrome for Testing app. On macOS its
|
|
302
310
|
// com.google.chrome.for.testing identity cannot intercept open/quit events for
|
|
303
|
-
// the user's com.google.Chrome browser. CI
|
|
304
|
-
// local rendering fails closed because that
|
|
305
|
-
// chooses SwiftShader. NOTE: this is NOT enough to make \`peach perf cpu\`
|
|
311
|
+
// the user's com.google.Chrome browser. CI and non-Mac hosts use
|
|
312
|
+
// chromium_headless_shell; local Mac rendering fails closed because that
|
|
313
|
+
// shell has no GPU and silently chooses SwiftShader. NOTE: this is NOT enough to make \`peach perf cpu\`
|
|
306
314
|
// work — that profiles the tenant *worker*, and the JS Self-Profiler API
|
|
307
315
|
// cannot be constructed in a dedicated worker (see
|
|
308
316
|
// worker-handlers/cpu-profile.ts). cpu-profile needs a CDP-based profiler
|
|
@@ -317,7 +325,8 @@ const cleanupProfile = () => {
|
|
|
317
325
|
throw new Error('[playwright-driver] refused SwiftShader launch arguments');
|
|
318
326
|
}
|
|
319
327
|
// one browser path per environment: the full, separately identified test app
|
|
320
|
-
//
|
|
328
|
+
// on a local Mac; the bundled chromium everywhere else. never launch a
|
|
329
|
+
// branded user browser.
|
|
321
330
|
const channelsToTry = hardwareGpuRequired ? ['chromium'] : [null];
|
|
322
331
|
const launchWith = async (channel) => {
|
|
323
332
|
const opts = channel
|
|
@@ -429,6 +438,13 @@ const cleanupProfile = () => {
|
|
|
429
438
|
await context.addInitScript({
|
|
430
439
|
content: 'Object.defineProperty(window, "__sootsimHostMeta", { value: ' + JSON.stringify(hostMeta) + ', configurable: true });' + bridgePortInject,
|
|
431
440
|
});
|
|
441
|
+
// ASWebAuthenticationSession completes on a navigation to its callback
|
|
442
|
+
// scheme, which a browser popup cannot follow.
|
|
443
|
+
await forwardPopupSchemeNavigations(
|
|
444
|
+
context,
|
|
445
|
+
${JSON.stringify(AUTH_BROWSER_EXTERNAL_NAVIGATION_EVENT)},
|
|
446
|
+
${JSON.stringify(AUTH_BROWSER_SESSION_FRAGMENT_KEY)},
|
|
447
|
+
);
|
|
432
448
|
await context.exposeBinding('__sootsimHostClose', async () => {
|
|
433
449
|
await shutdown(0);
|
|
434
450
|
});
|
package/cli/main.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// single lazy-imported command a run dispatches to. `./bin` imports this only
|
|
3
3
|
// when none of its hidden entry points claimed the process.
|
|
4
4
|
|
|
5
|
+
import { automaticPeachUpdatesEnabled } from '../src/automatic-update-policy'
|
|
5
6
|
import { rnxPublicBrand } from '../src/public-brand'
|
|
6
7
|
import { levenshtein } from './levenshtein'
|
|
7
8
|
import { COMMANDS, parseArgs, TOP_LEVEL_RUNTIME_COMMANDS } from './parse-args'
|
|
@@ -21,7 +22,10 @@ import { IS_STANDALONE } from './standalone'
|
|
|
21
22
|
// "command timed out after 10s" message and is uncopyable for a bug report.
|
|
22
23
|
// rejections that don't match this shape still get the verbose stack so we
|
|
23
24
|
// don't paper over real bugs.
|
|
24
|
-
function handleTopLevelError(err: unknown): never {
|
|
25
|
+
async function handleTopLevelError(err: unknown): Promise<never> {
|
|
26
|
+
const { trackCliError, flushCliTelemetry } = await import('./telemetry')
|
|
27
|
+
trackCliError(err)
|
|
28
|
+
await flushCliTelemetry()
|
|
25
29
|
const msg = err instanceof Error ? err.message : String(err)
|
|
26
30
|
// peach's known top-level errors are all single-line, narrow strings.
|
|
27
31
|
// everything else gets the full stack so unexpected crashes still surface.
|
|
@@ -92,7 +96,7 @@ const isReadOnlyInvocation =
|
|
|
92
96
|
parsed.commandArgs.includes('--dry-run')
|
|
93
97
|
|
|
94
98
|
if (isCliUpdateWorker) {
|
|
95
|
-
if (IS_STANDALONE) {
|
|
99
|
+
if (IS_STANDALONE && automaticPeachUpdatesEnabled()) {
|
|
96
100
|
try {
|
|
97
101
|
const { refreshCliUpdateCache } = await import('./cli-update')
|
|
98
102
|
await refreshCliUpdateCache()
|
|
@@ -119,7 +123,12 @@ if (
|
|
|
119
123
|
// interactive command runs next prints it once (stderr, so --json stdout
|
|
120
124
|
// stays parseable) and deletes it. the daemon/serve processes skip it so a
|
|
121
125
|
// background process can't eat the notice before a human sees it.
|
|
122
|
-
if (
|
|
126
|
+
if (
|
|
127
|
+
automaticPeachUpdatesEnabled() &&
|
|
128
|
+
!isReadOnlyInvocation &&
|
|
129
|
+
parsed.command !== 'serve' &&
|
|
130
|
+
parsed.command !== 'daemon'
|
|
131
|
+
) {
|
|
123
132
|
const { consumeRuntimeUpgradeNotice } = await import('../src/home-paths')
|
|
124
133
|
const notice = consumeRuntimeUpgradeNotice()
|
|
125
134
|
if (notice) {
|
|
@@ -589,6 +598,11 @@ try {
|
|
|
589
598
|
break
|
|
590
599
|
}
|
|
591
600
|
|
|
601
|
+
case 'project-report': {
|
|
602
|
+
const { runProjectReport } = await import('./commands/project-report')
|
|
603
|
+
await exitWithFlush(runProjectReport(parsed.commandArgs))
|
|
604
|
+
}
|
|
605
|
+
|
|
592
606
|
case 'report-issue': {
|
|
593
607
|
const { runReportIssue } = await import('./commands/report-issue')
|
|
594
608
|
const code = await runReportIssue(parsed.commandArgs)
|
|
@@ -708,5 +722,7 @@ try {
|
|
|
708
722
|
throw error
|
|
709
723
|
}
|
|
710
724
|
|
|
725
|
+
const { flushCliTelemetry } = await import('./telemetry')
|
|
726
|
+
await flushCliTelemetry()
|
|
711
727
|
await scheduleAutomaticCliUpdate(0)
|
|
712
728
|
await scheduleAutomaticCleanup()
|
|
@@ -38,7 +38,8 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
|
|
|
38
38
|
category: 'bounded-diagnostics',
|
|
39
39
|
destination: 'configured PostHog ingestion host',
|
|
40
40
|
preConsent: false,
|
|
41
|
-
payload:
|
|
41
|
+
payload:
|
|
42
|
+
'anonymous install id, CLI/runtime versions, OS/arch, command name, package compatibility fields, error class and bounded path-redacted first-line error/warning messages',
|
|
42
43
|
},
|
|
43
44
|
contrast_user_action: {
|
|
44
45
|
category: 'explicit-user-action',
|
|
@@ -349,12 +350,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
349
350
|
category: 'guest_app_network',
|
|
350
351
|
count: 1,
|
|
351
352
|
},
|
|
352
|
-
'packages/peach-engine/src/engine/config-plugin-splash-loader.ts :: fetch :: manifestFetchUrl(origin)':
|
|
353
|
-
{ category: 'local_development', count: 1 },
|
|
354
353
|
'packages/peach-engine/src/engine/emoji-metrics-font-loader.ts :: fetch :: attemptedBundledUrl':
|
|
355
354
|
{ category: 'same_origin_runtime', count: 1 },
|
|
356
|
-
'packages/peach-engine/src/engine/expo-app-config.ts :: fetch :: manifestFetchUrl(origin)':
|
|
357
|
-
{ category: 'local_development', count: 1 },
|
|
358
355
|
'packages/peach-engine/src/engine/image-loader.ts :: fetch :: resolveImageFetchUri(uri)':
|
|
359
356
|
{ category: 'guest_app_network', count: 1 },
|
|
360
357
|
'packages/peach-engine/src/engine/language-font-loader.ts :: fetch :: url': {
|
package/cli/privacy.ts
CHANGED
|
@@ -23,7 +23,6 @@ export function diagnosticsAllowedForCurrentProcess(
|
|
|
23
23
|
): boolean {
|
|
24
24
|
const doNotTrack = process.env.DO_NOT_TRACK
|
|
25
25
|
if (doNotTrack === '1' || doNotTrack === 'true') return false
|
|
26
|
-
if (!canPromptForPrivacyChoice()) return false
|
|
27
26
|
return preferences.configured && preferences.productAnalytics
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -42,7 +41,7 @@ export async function ensurePrivacyChoice(options: {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
const allowed = await confirm(
|
|
45
|
-
'Share anonymous package incompatibilities and error
|
|
44
|
+
'Share anonymous diagnostics, including CI and agent runs: CLI/runtime versions, OS/arch, command name, random install ID, package incompatibilities, error class, and first-line error/warning messages (300 characters, paths redacted; no arguments, stacks, source or structured app data)?',
|
|
46
45
|
false,
|
|
47
46
|
)
|
|
48
47
|
// the engine's historical analytics and crash transports carry broader
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { consoleEntriesEval, type InspectBridge } from './commands/inspect/core'
|
|
2
|
+
import { diagnosticsAllowedForCurrentProcess } from './privacy'
|
|
3
|
+
import { setDiagnosticRuntimeVersion, trackCliDiagnostic } from './telemetry'
|
|
4
|
+
|
|
5
|
+
// reuse the same local console/error stores the CLI already inspects. only
|
|
6
|
+
// message strings cross into the diagnostic queue, never stacks or objects.
|
|
7
|
+
export async function collectRuntimeDiagnostics(bridge: InspectBridge): Promise<void> {
|
|
8
|
+
if (!diagnosticsAllowedForCurrentProcess()) return
|
|
9
|
+
try {
|
|
10
|
+
const result: unknown = await bridge.send(
|
|
11
|
+
{
|
|
12
|
+
type: 'evaluate',
|
|
13
|
+
code: `(() => {
|
|
14
|
+
const messages = []
|
|
15
|
+
const add = (kind, message) => {
|
|
16
|
+
if (typeof message === 'string' && message) messages.push({ kind, message: message.slice(0, 2048) })
|
|
17
|
+
}
|
|
18
|
+
const obs = window.__sootsimObservability
|
|
19
|
+
for (const e of (obs?.reportedErrors?.getSnapshot?.() || []).slice(-20)) add('runtime_error', e.message)
|
|
20
|
+
for (const [kind, entries] of [
|
|
21
|
+
['runtime_error', ${consoleEntriesEval('error', 20)}],
|
|
22
|
+
['runtime_warning', ${consoleEntriesEval('warn', 20)}],
|
|
23
|
+
]) {
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
const parts = e.args.map(a => typeof a === 'string' ? a : typeof a?.message === 'string' ? a.message : '').filter(Boolean)
|
|
26
|
+
add(kind, parts.join(' '))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { runtimeVersion: window.__sootsimRuntimeVersion, messages: messages.slice(0, 50) }
|
|
30
|
+
})()`,
|
|
31
|
+
},
|
|
32
|
+
{ timeoutMs: 1000 },
|
|
33
|
+
)
|
|
34
|
+
if (!result || typeof result !== 'object') return
|
|
35
|
+
if ('runtimeVersion' in result && typeof result.runtimeVersion === 'string') {
|
|
36
|
+
setDiagnosticRuntimeVersion(result.runtimeVersion)
|
|
37
|
+
}
|
|
38
|
+
if (!('messages' in result) || !Array.isArray(result.messages)) return
|
|
39
|
+
for (const entry of result.messages.slice(0, 50)) {
|
|
40
|
+
if (!entry || typeof entry !== 'object') continue
|
|
41
|
+
if (
|
|
42
|
+
(entry.kind === 'runtime_error' || entry.kind === 'runtime_warning') &&
|
|
43
|
+
typeof entry.message === 'string'
|
|
44
|
+
) {
|
|
45
|
+
trackCliDiagnostic({ kind: entry.kind, message: entry.message })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// diagnostics never delay a failed bridge beyond this bounded probe.
|
|
50
|
+
}
|
|
51
|
+
}
|
package/cli/telemetry.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { getCliVersion } from '../src/cli-version'
|
|
3
|
+
import { readActiveRuntime, readOrCreateAnonymousId } from '../src/home-paths'
|
|
4
|
+
import { parseArgs } from './parse-args'
|
|
1
5
|
import { diagnosticsAllowedForCurrentProcess } from './privacy'
|
|
2
6
|
|
|
3
7
|
type PackageIncompatibility = {
|
|
@@ -10,9 +14,15 @@ type PackageIncompatibility = {
|
|
|
10
14
|
type ErrorShape = {
|
|
11
15
|
kind: 'error_shape'
|
|
12
16
|
errorClass: string
|
|
17
|
+
message: string
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
type RuntimeMessage = {
|
|
21
|
+
kind: 'runtime_error' | 'runtime_warning'
|
|
22
|
+
message: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type CliDiagnostic = PackageIncompatibility | ErrorShape | RuntimeMessage
|
|
16
26
|
|
|
17
27
|
type Config = { apiKey: string; host: string }
|
|
18
28
|
|
|
@@ -32,6 +42,46 @@ function bounded(value: string, max: number): string {
|
|
|
32
42
|
return value.trim().slice(0, max)
|
|
33
43
|
}
|
|
34
44
|
|
|
45
|
+
export function sanitizeDiagnosticMessage(value: string): string {
|
|
46
|
+
const firstLine = value.slice(0, 2048).split(/[\r\n\u2028\u2029]/, 1)[0] ?? ''
|
|
47
|
+
// retain package names from dependency paths, never source filenames,
|
|
48
|
+
// URL paths/queries, local paths, stack frames, or serialized app objects.
|
|
49
|
+
return firstLine
|
|
50
|
+
.split(homedir())
|
|
51
|
+
.join('<home>')
|
|
52
|
+
.replace(
|
|
53
|
+
/<package:(?:@[\w.-]+\/)?[\w.-]+>|(?:https?:\/\/|file:\/\/|[A-Za-z]:[\\/]|<home>[\\/]|\/|\.{1,2}[\\/]|\b[\w.-]+[\\/])[^\s"'<>]+/g,
|
|
54
|
+
(location) => {
|
|
55
|
+
if (location.startsWith('<package:')) return location
|
|
56
|
+
const dependency = /(?:^|[\\/])node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
|
|
57
|
+
location,
|
|
58
|
+
)
|
|
59
|
+
return dependency ? `<package:${dependency[1]?.replace(/\\/g, '/')}>` : '<path>'
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
.replace(/\b[\w.-]+\.(?:tsx?|jsx?|swift|m|mm|kt|java)(?::\d+(?::\d+)?)?/g, '<source>')
|
|
63
|
+
.replace(/\{.*$|\[(?:\s*["'{\d]|$).*$/g, '<data>')
|
|
64
|
+
.replace(
|
|
65
|
+
/\b(?:const|let|var)\s+\w+\s*=.*$|\b(?:function|class)\s+\w+.*$|\b(?:import|export)\s+.*$|\breturn\s*[({<].*$/g,
|
|
66
|
+
'<source>',
|
|
67
|
+
)
|
|
68
|
+
.replace(/^\s*at\s+.*$|\bat\s+[\w.$<>]+\s*\([^)]*\)/g, '<stack>')
|
|
69
|
+
.trim()
|
|
70
|
+
.slice(0, 300)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let observedRuntimeVersion: string | null = null
|
|
74
|
+
|
|
75
|
+
function diagnosticVersion(version: string | null): string | null {
|
|
76
|
+
return version && version.length <= 80 && /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version)
|
|
77
|
+
? version
|
|
78
|
+
: null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function setDiagnosticRuntimeVersion(version: string): void {
|
|
82
|
+
observedRuntimeVersion = diagnosticVersion(version)
|
|
83
|
+
}
|
|
84
|
+
|
|
35
85
|
export function buildCliDiagnosticEvent(diagnostic: CliDiagnostic) {
|
|
36
86
|
const properties =
|
|
37
87
|
diagnostic.kind === 'package_incompatibility'
|
|
@@ -42,12 +92,24 @@ export function buildCliDiagnosticEvent(diagnostic: CliDiagnostic) {
|
|
|
42
92
|
? { unsupported_api: bounded(diagnostic.unsupportedApi, 160) }
|
|
43
93
|
: {}),
|
|
44
94
|
}
|
|
45
|
-
:
|
|
95
|
+
: diagnostic.kind === 'error_shape'
|
|
96
|
+
? {
|
|
97
|
+
error_class: bounded(diagnostic.errorClass, 160),
|
|
98
|
+
message: sanitizeDiagnosticMessage(diagnostic.message),
|
|
99
|
+
}
|
|
100
|
+
: { message: sanitizeDiagnosticMessage(diagnostic.message) }
|
|
46
101
|
|
|
47
102
|
return {
|
|
48
103
|
event: diagnostic.kind,
|
|
49
|
-
distinct_id:
|
|
50
|
-
properties
|
|
104
|
+
distinct_id: readOrCreateAnonymousId(),
|
|
105
|
+
properties: {
|
|
106
|
+
cli_version: diagnosticVersion(getCliVersion()),
|
|
107
|
+
runtime_version: observedRuntimeVersion ?? diagnosticVersion(readActiveRuntime()),
|
|
108
|
+
os: process.platform,
|
|
109
|
+
arch: process.arch,
|
|
110
|
+
command: parseArgs(process.argv).command ?? 'open',
|
|
111
|
+
...properties,
|
|
112
|
+
},
|
|
51
113
|
}
|
|
52
114
|
}
|
|
53
115
|
|
|
@@ -55,12 +117,20 @@ const queue: CliDiagnostic[] = []
|
|
|
55
117
|
|
|
56
118
|
export function trackCliDiagnostic(diagnostic: CliDiagnostic): void {
|
|
57
119
|
if (!diagnosticsAllowedForCurrentProcess()) return
|
|
58
|
-
queue.
|
|
120
|
+
if (queue.length >= 50) return
|
|
121
|
+
const entry =
|
|
122
|
+
diagnostic.kind === 'package_incompatibility'
|
|
123
|
+
? diagnostic
|
|
124
|
+
: { ...diagnostic, message: sanitizeDiagnosticMessage(diagnostic.message) }
|
|
125
|
+
if (queue.some((queued) => JSON.stringify(queued) === JSON.stringify(entry))) return
|
|
126
|
+
queue.push(entry)
|
|
59
127
|
}
|
|
60
128
|
|
|
61
129
|
export function trackCliError(error: unknown): void {
|
|
62
130
|
const errorClass = error instanceof Error ? error.constructor.name : typeof error
|
|
63
|
-
|
|
131
|
+
const message =
|
|
132
|
+
error instanceof Error ? error.message : typeof error === 'string' ? error : ''
|
|
133
|
+
trackCliDiagnostic({ kind: 'error_shape', errorClass, message })
|
|
64
134
|
}
|
|
65
135
|
|
|
66
136
|
export async function flushCliTelemetry(): Promise<void> {
|
|
@@ -70,8 +140,9 @@ export async function flushCliTelemetry(): Promise<void> {
|
|
|
70
140
|
queue.length = 0
|
|
71
141
|
return
|
|
72
142
|
}
|
|
73
|
-
const
|
|
143
|
+
const diagnostics = queue.splice(0, queue.length)
|
|
74
144
|
try {
|
|
145
|
+
const batch = diagnostics.map((diagnostic) => buildCliDiagnosticEvent(diagnostic))
|
|
75
146
|
await fetch(`${config.host}/batch/`, {
|
|
76
147
|
method: 'POST',
|
|
77
148
|
headers: { 'content-type': 'application/json' },
|
package/detox/index.ts
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
readBrowserCompositeCaptureRequest,
|
|
11
11
|
resolveBrowserCompositeCdpClip,
|
|
12
12
|
} from '../../peach-engine/src/capture/browser-composite'
|
|
13
|
+
import {
|
|
14
|
+
AUTH_BROWSER_EXTERNAL_NAVIGATION_EVENT,
|
|
15
|
+
AUTH_BROWSER_SESSION_FRAGMENT_KEY,
|
|
16
|
+
forwardPopupSchemeNavigations,
|
|
17
|
+
} from '../../peach-engine/src/open-external'
|
|
13
18
|
import { isAppBootTargetUrl } from '../../peach-engine/src/render-worker/app-load'
|
|
14
19
|
import { SOOTSIM_SHELL_COMMAND_NAME } from '../src/menu'
|
|
15
20
|
import { pollUntil } from '../src/poll-until'
|
|
@@ -55,7 +60,6 @@ let _reapBrowserNow: (() => void) | null = null
|
|
|
55
60
|
let _context: BrowserContext | null = null
|
|
56
61
|
let _page: Page | null = null
|
|
57
62
|
let _synchronizationEnabled = false
|
|
58
|
-
let _suspendedAppId: string | null = null
|
|
59
63
|
let _browserCompromised: string | null = null
|
|
60
64
|
let browserIdentity = 0
|
|
61
65
|
let contextIdentity = 0
|
|
@@ -601,7 +605,7 @@ export async function waitForSootsimIdle(
|
|
|
601
605
|
// find a node in the sootsim tree matching a matcher descriptor
|
|
602
606
|
async function findNodeByMatcher(matcher: Matcher): Promise<any> {
|
|
603
607
|
const page = getPage()
|
|
604
|
-
if (matcher.index !== undefined) {
|
|
608
|
+
if (matcher.index !== undefined || matcher.type === 'text') {
|
|
605
609
|
return page.evaluate(async (indexedMatcher: Matcher) => {
|
|
606
610
|
const filter =
|
|
607
611
|
indexedMatcher.type === 'id'
|
|
@@ -614,8 +618,8 @@ async function findNodeByMatcher(matcher: Matcher): Promise<any> {
|
|
|
614
618
|
? { hasRole: indexedMatcher.value }
|
|
615
619
|
: { type: indexedMatcher.value }
|
|
616
620
|
const results = await window.__sootsimTest!.queryAll(filter)
|
|
617
|
-
// detox
|
|
618
|
-
//
|
|
621
|
+
// detox by.text matches exact text elements, with or without an index.
|
|
622
|
+
// hasText also matches ancestors and substrings, which are not targets.
|
|
619
623
|
const indexed =
|
|
620
624
|
indexedMatcher.type === 'text'
|
|
621
625
|
? results.filter(
|
|
@@ -630,11 +634,6 @@ async function findNodeByMatcher(matcher: Matcher): Promise<any> {
|
|
|
630
634
|
(id: string) => window.__sootsimTest!.findByTestId(id),
|
|
631
635
|
matcher.value,
|
|
632
636
|
)
|
|
633
|
-
} else if (matcher.type === 'text') {
|
|
634
|
-
return page.evaluate(
|
|
635
|
-
(text: string) => window.__sootsimTest!.findByText(text),
|
|
636
|
-
matcher.value,
|
|
637
|
-
)
|
|
638
637
|
} else if (matcher.type === 'label') {
|
|
639
638
|
return page.evaluate(
|
|
640
639
|
(label: string) => window.__sootsimTest!.findByLabel(label),
|
|
@@ -933,7 +932,22 @@ async function dispatchDeviceOrientation(orientation: 'portrait' | 'landscape')
|
|
|
933
932
|
landscapeSide: DETOX_PLATFORM === 'ios' ? 'right' : 'left',
|
|
934
933
|
},
|
|
935
934
|
)
|
|
936
|
-
|
|
935
|
+
// destination dimensions publish before the interface finishes turning.
|
|
936
|
+
await page.waitForFunction(
|
|
937
|
+
(next) => {
|
|
938
|
+
const settings = window.SootSim?.bridges?.settings?.get?.()
|
|
939
|
+
const rotation = Reflect.get(window, '__sootsimDeviceRotation')
|
|
940
|
+
return (
|
|
941
|
+
typeof settings === 'object' &&
|
|
942
|
+
settings !== null &&
|
|
943
|
+
'orientation' in settings &&
|
|
944
|
+
settings.orientation === next &&
|
|
945
|
+
rotation?.active !== true
|
|
946
|
+
)
|
|
947
|
+
},
|
|
948
|
+
orientation,
|
|
949
|
+
{ timeout: 15000 },
|
|
950
|
+
)
|
|
937
951
|
}
|
|
938
952
|
|
|
939
953
|
const MAX_SETTLED_SPEED_PT_PER_SEC = 2.0
|
|
@@ -1584,14 +1598,18 @@ function createSootSimElement(matcher: Matcher): SootSimElement {
|
|
|
1584
1598
|
visible: node.layout.width > 0 && node.layout.height > 0,
|
|
1585
1599
|
enabled: !node.accessibilityState?.disabled,
|
|
1586
1600
|
...node,
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1601
|
+
// the transformed rect when the node reports one; a node resolved
|
|
1602
|
+
// without a size (x and y only) keeps its own frame
|
|
1603
|
+
frame:
|
|
1604
|
+
node.absolutePosition?.width !== undefined &&
|
|
1605
|
+
node.absolutePosition.height !== undefined
|
|
1606
|
+
? {
|
|
1607
|
+
x: node.absolutePosition.x,
|
|
1608
|
+
y: node.absolutePosition.y,
|
|
1609
|
+
width: node.absolutePosition.width,
|
|
1610
|
+
height: node.absolutePosition.height,
|
|
1611
|
+
}
|
|
1612
|
+
: node.frame,
|
|
1595
1613
|
}
|
|
1596
1614
|
},
|
|
1597
1615
|
|
|
@@ -1755,36 +1773,41 @@ export const device = {
|
|
|
1755
1773
|
if (_browserCompromised) {
|
|
1756
1774
|
throw new Error(`sootsim browser is compromised: ${_browserCompromised}`)
|
|
1757
1775
|
}
|
|
1758
|
-
if (
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
) {
|
|
1776
|
+
if (_page && opts?.newInstance === false && !opts.delete && !opts.recordVideoDir) {
|
|
1777
|
+
// detox activates the running app without relaunching it; a url would
|
|
1778
|
+
// need a delivery path into the running guest that the adapter lacks.
|
|
1779
|
+
if (opts.url) {
|
|
1780
|
+
throw new Error(`SootSim cannot deliver a url to a running app: ${opts.url}`)
|
|
1781
|
+
}
|
|
1765
1782
|
const page = _page
|
|
1766
|
-
const appId =
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
()
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
)
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1783
|
+
const appId = await page.evaluate(async () => {
|
|
1784
|
+
const state = await window.SootSim?.bridges?.mainShell?.getState?.()
|
|
1785
|
+
const guestApp = state?.guestApp
|
|
1786
|
+
if (typeof guestApp !== 'string') {
|
|
1787
|
+
throw new Error('SootSim main shell reports no guest app')
|
|
1788
|
+
}
|
|
1789
|
+
if (state?.state === 'app' && state.activeApp === guestApp) return null
|
|
1790
|
+
window.SootSim?.bridges?.mainShell?.launchApp?.(guestApp)
|
|
1791
|
+
return guestApp
|
|
1792
|
+
})
|
|
1793
|
+
if (appId !== null) {
|
|
1794
|
+
await pollUntil(
|
|
1795
|
+
() =>
|
|
1796
|
+
page.evaluate(async (id) => {
|
|
1797
|
+
const state = await window.SootSim?.bridges?.mainShell?.getState?.()
|
|
1798
|
+
return (
|
|
1799
|
+
state?.state === 'app' &&
|
|
1800
|
+
state.activeApp === id &&
|
|
1801
|
+
state.switcherPhase === 'idle'
|
|
1802
|
+
)
|
|
1803
|
+
}, appId),
|
|
1804
|
+
Boolean,
|
|
1805
|
+
{
|
|
1806
|
+
timeoutMs: 10000,
|
|
1807
|
+
label: `SootSim app ${appId} to resume`,
|
|
1808
|
+
},
|
|
1809
|
+
)
|
|
1810
|
+
}
|
|
1788
1811
|
await waitForSootsimTree(page, 10000)
|
|
1789
1812
|
if (opts?.userNotification) {
|
|
1790
1813
|
await postRemotePushToSimulator(opts.userNotification, true)
|
|
@@ -1833,6 +1856,11 @@ export const device = {
|
|
|
1833
1856
|
reapOwnedBrowser()
|
|
1834
1857
|
throw error
|
|
1835
1858
|
}
|
|
1859
|
+
await forwardPopupSchemeNavigations(
|
|
1860
|
+
_context,
|
|
1861
|
+
AUTH_BROWSER_EXTERNAL_NAVIGATION_EVENT,
|
|
1862
|
+
AUTH_BROWSER_SESSION_FRAGMENT_KEY,
|
|
1863
|
+
)
|
|
1836
1864
|
await _context.exposeBinding('__sootsimHostCapture', async (source, value) => {
|
|
1837
1865
|
const request = readBrowserCompositeCaptureRequest(value)
|
|
1838
1866
|
const session = await source.page.context().newCDPSession(source.page)
|
|
@@ -3178,14 +3206,12 @@ export const device = {
|
|
|
3178
3206
|
|
|
3179
3207
|
async sendToHome() {
|
|
3180
3208
|
const page = getPage()
|
|
3181
|
-
|
|
3209
|
+
await page.evaluate(async () => {
|
|
3182
3210
|
const state = await window.SootSim?.bridges?.mainShell?.getState?.()
|
|
3183
|
-
|
|
3184
|
-
if (typeof appId !== 'string') {
|
|
3211
|
+
if (typeof state?.activeApp !== 'string') {
|
|
3185
3212
|
throw new Error(`cannot send an inactive SootSim app home`)
|
|
3186
3213
|
}
|
|
3187
3214
|
window.SootSim?.bridges?.mainShell?.goHome?.()
|
|
3188
|
-
return appId
|
|
3189
3215
|
})
|
|
3190
3216
|
await pollUntil(
|
|
3191
3217
|
() =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.631 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.mjs
CHANGED