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,341 @@
|
|
|
1
|
+
// native-seam manifest — the canonical registry of upstream packages whose
|
|
2
|
+
// native-module spec files we override.
|
|
3
|
+
//
|
|
4
|
+
// the rule: when a guest bundle imports from one of these packages, vite lets
|
|
5
|
+
// the package's JS resolve from node_modules normally. only the listed
|
|
6
|
+
// `seamBasenames` are redirected to our stub. this keeps the upstream JS layer
|
|
7
|
+
// (hooks, helpers, transforms, listeners) running unchanged.
|
|
8
|
+
//
|
|
9
|
+
// adding a new entry:
|
|
10
|
+
// 1. identify upstream's native-module file (look for files calling
|
|
11
|
+
// `requireNativeModule`, `TurboModuleRegistry.getEnforced`,
|
|
12
|
+
// `requireNativeComponent`, or in `src/specs/Native*Module.ts`)
|
|
13
|
+
// 2. write a thin stub at `packages/compat/src/stubs/native-seams/<pkg>.ts`
|
|
14
|
+
// that exports the same default + named exports upstream's seam file
|
|
15
|
+
// exports — except the implementation calls into sootsim's engine.
|
|
16
|
+
// 3. append an entry below. the plugin handles the rest.
|
|
17
|
+
//
|
|
18
|
+
// canonical example: react-native-reanimated. see
|
|
19
|
+
// - `packages/compat/src/stubs/react-native-reanimated.ts` (the seam stub)
|
|
20
|
+
// - `reanimatedNativeSeamRedirect` in vite-plugin.ts (still hard-coded
|
|
21
|
+
// because it has package-specific edge cases — fabricUtils,
|
|
22
|
+
// validate-worklets-version. new entries should use this manifest path
|
|
23
|
+
// for the simple basename-match case.)
|
|
24
|
+
|
|
25
|
+
export interface NativeSeamEntry {
|
|
26
|
+
/**
|
|
27
|
+
* upstream npm package name. all imports whose importer path includes
|
|
28
|
+
* `/<pkg>/` are candidates for redirect.
|
|
29
|
+
*/
|
|
30
|
+
pkg: string
|
|
31
|
+
/**
|
|
32
|
+
* basenames of upstream files to redirect. each entry matches the file
|
|
33
|
+
* name (no extension, no platform suffix like `.ios` / `.android`) of any
|
|
34
|
+
* upstream module imported transitively from within `pkg`.
|
|
35
|
+
*
|
|
36
|
+
* example: if upstream has
|
|
37
|
+
* `node_modules/expo-notifications/src/NotificationsHandler.ts`, list
|
|
38
|
+
* `'NotificationsHandler'` here and we'll redirect any import resolving
|
|
39
|
+
* to that file to our seam stub.
|
|
40
|
+
*/
|
|
41
|
+
seamBasenames: string[]
|
|
42
|
+
/**
|
|
43
|
+
* absolute path to the seam stub. typically
|
|
44
|
+
* `packages/compat/src/stubs/native-seams/<pkg>.ts` — but a single stub
|
|
45
|
+
* file may export every native module the package exposes, so the same
|
|
46
|
+
* `target` can appear in multiple entries if a package has multiple native
|
|
47
|
+
* modules that share an implementation file.
|
|
48
|
+
*
|
|
49
|
+
* authors set this via the `seamPath(<pkg>)` helper below.
|
|
50
|
+
*/
|
|
51
|
+
target: string
|
|
52
|
+
/**
|
|
53
|
+
* notes — short reason this entry exists. shown by the debug logger when
|
|
54
|
+
* `SOOTSIM_NATIVE_SEAM_DEBUG=1` is set.
|
|
55
|
+
*/
|
|
56
|
+
notes?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
import fs from 'node:fs'
|
|
60
|
+
import path from 'node:path'
|
|
61
|
+
import { fileURLToPath } from 'node:url'
|
|
62
|
+
|
|
63
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
64
|
+
const NATIVE_SEAMS_DIR = path.resolve(HERE, '../../compat/src/stubs/native-seams')
|
|
65
|
+
|
|
66
|
+
/** resolve a stub path under `packages/compat/src/stubs/native-seams/`.
|
|
67
|
+
* resolves to `.tsx` when only that exists (JSX-bearing seams like
|
|
68
|
+
* expo-image, react-native-webview). preferred order: `.ts`, `.tsx`. */
|
|
69
|
+
export function seamPath(basename: string): string {
|
|
70
|
+
const ts = path.resolve(NATIVE_SEAMS_DIR, `${basename}.ts`)
|
|
71
|
+
if (fs.existsSync(ts)) return ts
|
|
72
|
+
const tsx = path.resolve(NATIVE_SEAMS_DIR, `${basename}.tsx`)
|
|
73
|
+
if (fs.existsSync(tsx)) return tsx
|
|
74
|
+
// fall through to .ts so vite's "Could not load" error names the .ts
|
|
75
|
+
// path the author probably intended to create, instead of silently
|
|
76
|
+
// pointing at something that doesn't exist with no extension.
|
|
77
|
+
return ts
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* the manifest. agents append entries here during the native-seam migration
|
|
82
|
+
* (plans/sootsim/compat/compat-stubs-native-seam-audit.md). keep alphabetical by `pkg` to
|
|
83
|
+
* minimise merge conflicts.
|
|
84
|
+
*/
|
|
85
|
+
export const NATIVE_SEAM_MANIFEST: NativeSeamEntry[] = [
|
|
86
|
+
// entries land here during the Phase 1 migration. example shape:
|
|
87
|
+
//
|
|
88
|
+
// {
|
|
89
|
+
// pkg: 'expo-notifications',
|
|
90
|
+
// seamBasenames: [
|
|
91
|
+
// 'NotificationsHandler',
|
|
92
|
+
// 'NotificationsEmitter',
|
|
93
|
+
// 'NotificationScheduler',
|
|
94
|
+
// 'NotificationCategoriesService',
|
|
95
|
+
// 'ServerRegistrationModule',
|
|
96
|
+
// ],
|
|
97
|
+
// target: seamPath('expo-notifications'),
|
|
98
|
+
// notes: 'route through engine NotificationBus',
|
|
99
|
+
// },
|
|
100
|
+
{
|
|
101
|
+
pkg: '@metamask/react-native-search-api',
|
|
102
|
+
// upstream is single-file `index.js`; no sub-imports inside the package
|
|
103
|
+
// to redirect at the basename layer. listed here for documentation parity
|
|
104
|
+
// — the bundle-loader's stub-registry routes the package-entry directly to
|
|
105
|
+
// `stubs/metamask-react-native-search-api.ts`, which pairs the upstream
|
|
106
|
+
// JS wrapper with the native seam below.
|
|
107
|
+
seamBasenames: ['index'],
|
|
108
|
+
target: seamPath('metamask-react-native-search-api'),
|
|
109
|
+
notes:
|
|
110
|
+
'iOS CoreSpotlight + NSUserActivity native seam; pure-JS wrapper class lives at stubs/metamask-react-native-search-api.ts',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
pkg: '@ua/react-native-airship',
|
|
114
|
+
seamBasenames: [
|
|
115
|
+
// src/NativeRNAirship.ts — the RNAirship TurboModule
|
|
116
|
+
'NativeRNAirship',
|
|
117
|
+
// codegen host components (no native backing in sootsim)
|
|
118
|
+
'RNAirshipEmbeddedViewNativeComponent',
|
|
119
|
+
'RNAirshipMessageViewNativeComponent',
|
|
120
|
+
],
|
|
121
|
+
target: seamPath('react-native-airship'),
|
|
122
|
+
notes:
|
|
123
|
+
'push-notification SAAS — noop the SAAS layer, route banners through NotificationBus',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
pkg: 'react-native-background-fetch',
|
|
127
|
+
// src/NativeBackgroundFetch.ts — TurboModuleRegistry.getEnforcing('RNBackgroundFetch')
|
|
128
|
+
seamBasenames: ['NativeBackgroundFetch'],
|
|
129
|
+
target: seamPath('react-native-background-fetch'),
|
|
130
|
+
notes:
|
|
131
|
+
'iOS BGTaskScheduler / Android WorkManager TurboModule — no real scheduler in browser; acknowledge registrations, never fire periodic events',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pkg: 'expo-apple-authentication',
|
|
135
|
+
// src/ExpoAppleAuthenticationButton.ts is the package's native-view
|
|
136
|
+
// boundary. direct-compiled apps do not run the fetched-bundle lazy seam
|
|
137
|
+
// loader, so route that boundary to the same view manager implementation.
|
|
138
|
+
seamBasenames: ['ExpoAppleAuthenticationButton'],
|
|
139
|
+
target: seamPath('expo-apple-authentication-view'),
|
|
140
|
+
notes:
|
|
141
|
+
'ASAuthorizationAppleIDButton native view — render the system button surface while upstream AppleAuthenticationButton.tsx remains unchanged',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
pkg: 'expo-asset',
|
|
145
|
+
// src/ExpoAsset.ts — `requireNativeModule('ExpoAsset')`; only
|
|
146
|
+
// `downloadAsync` is on the native surface. all the rest (`Asset`,
|
|
147
|
+
// `AssetHooks`, `AssetSourceResolver`, ...) is pure JS that composes on
|
|
148
|
+
// top.
|
|
149
|
+
seamBasenames: ['ExpoAsset'],
|
|
150
|
+
target: seamPath('expo-asset'),
|
|
151
|
+
notes:
|
|
152
|
+
'expo-modules-core requireNativeModule(ExpoAsset) — only downloadAsync; mirror upstream web build (return remote URL)',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
pkg: '@bam.tech/react-native-image-resizer',
|
|
156
|
+
// upstream is not shipped to most guest bundles via node_modules — it
|
|
157
|
+
// arrives through the bundle-loader stub-registry which routes the
|
|
158
|
+
// package entry to `stubs/bam-react-native-image-resizer.ts`. listed here
|
|
159
|
+
// for documentation parity; vite-path redirect would key on
|
|
160
|
+
// `NativeImageResizer`.
|
|
161
|
+
seamBasenames: ['NativeImageResizer'],
|
|
162
|
+
target: seamPath('bam-react-native-image-resizer'),
|
|
163
|
+
notes:
|
|
164
|
+
'ImageResizer TurboModule — browser-side canvas resize implementing the iOS contain/cover/stretch + onlyScaleDown semantics from ios/ImageResizer.mm',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
pkg: 'react-native-adjust',
|
|
168
|
+
// upstream's `index.js` reads `NativeModules.Adjust` and
|
|
169
|
+
// `NativeModules.AdjustEventEmitter` directly — there is no NativeAdjust
|
|
170
|
+
// spec file. listed for documentation parity; the actual seam delivery is
|
|
171
|
+
// through the bundle-loader stub-registry routing the package entry to
|
|
172
|
+
// `stubs/react-native-adjust.ts`, which imports the native seam from this
|
|
173
|
+
// target.
|
|
174
|
+
seamBasenames: ['index'],
|
|
175
|
+
target: seamPath('react-native-adjust'),
|
|
176
|
+
notes:
|
|
177
|
+
'Adjust attribution SAAS — noop native side (Adjust + AdjustEventEmitter NativeModules), pure-JS classes (AdjustConfig/AdjustEvent/...) live in stubs/react-native-adjust.ts',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
pkg: 'expo-clipboard',
|
|
181
|
+
// src/ExpoClipboard.ts — `requireNativeModule<ExpoClipboardModule>('ExpoClipboard')`.
|
|
182
|
+
// upstream also ships `ExpoClipboard.web.ts` (which we mirror); the
|
|
183
|
+
// redirect catches both `.ts` and `.web.ts` since basenames match.
|
|
184
|
+
seamBasenames: ['ExpoClipboard'],
|
|
185
|
+
target: seamPath('expo-clipboard'),
|
|
186
|
+
notes:
|
|
187
|
+
'expo-modules-core requireNativeModule(ExpoClipboard) — real navigator.clipboard text + ClipboardItem image; mirrors upstream ExpoClipboard.web.ts',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
pkg: 'expo-font',
|
|
191
|
+
// src/ExpoFontLoader.ts — `requireNativeModule<ExpoFontLoaderModule>('ExpoFontLoader')`.
|
|
192
|
+
// we mirror upstream's web variant `ExpoFontLoader.web.ts` (FontFace API
|
|
193
|
+
// + managed <style id="expo-generated-fonts">).
|
|
194
|
+
seamBasenames: ['ExpoFontLoader'],
|
|
195
|
+
target: seamPath('expo-font'),
|
|
196
|
+
notes:
|
|
197
|
+
'expo-modules-core requireNativeModule(ExpoFontLoader) — FontFace API registration; mirrors upstream ExpoFontLoader.web.ts',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
pkg: '@react-native-async-storage/async-storage',
|
|
201
|
+
// src/NativeAsyncStorageModule.ts — TurboModule spec for `RNCAsyncStorage`.
|
|
202
|
+
// src/RCTAsyncStorage.ts — legacy `NativeModules['RNCAsyncStorage']` resolver.
|
|
203
|
+
// both basenames redirect to the same seam; upstream's AsyncStorage.ts
|
|
204
|
+
// imports default from either depending on bridgeless vs paper.
|
|
205
|
+
seamBasenames: ['NativeAsyncStorageModule', 'RCTAsyncStorage'],
|
|
206
|
+
target: seamPath('async-storage'),
|
|
207
|
+
notes:
|
|
208
|
+
'RNCAsyncStorage TurboModule — localStorage-backed; preserves multiMerge JSON deep-merge semantics from upstream iOS module',
|
|
209
|
+
},
|
|
210
|
+
// expo-calendar and expo-contacts deliberately have NO entry here. expo
|
|
211
|
+
// renamed their native module ids across majors (`ExpoCalendar` →
|
|
212
|
+
// `CalendarNext`, `ExpoContacts` → `ExpoContactsNext`) and kept the old
|
|
213
|
+
// ids serving the `/legacy` subpath, so one package now asks for two
|
|
214
|
+
// different native modules depending on which entry point you import.
|
|
215
|
+
// basename matching cannot tell `src/ExpoCalendar.ts` from
|
|
216
|
+
// `src/legacy/ExpoCalendar.ts`, so a redirect here forces the legacy API
|
|
217
|
+
// onto the class-based seam, whose methods throw.
|
|
218
|
+
//
|
|
219
|
+
// registering by module NAME instead is what keeps old and new package
|
|
220
|
+
// versions both working: ../../compat/src/stubs/expo-{calendar,contacts}-register.ts
|
|
221
|
+
// installs every id either version asks for, and upstream's own
|
|
222
|
+
// `requireNativeModule` picks the right one per entry point. a file
|
|
223
|
+
// redirect can only match one layout, so it breaks the versions it was
|
|
224
|
+
// not written for.
|
|
225
|
+
{
|
|
226
|
+
pkg: 'expo-media-library',
|
|
227
|
+
// src/ExpoMediaLibrary.ts — `requireNativeModule('ExpoMediaLibrary')`.
|
|
228
|
+
// seam mirrors upstream's `ExpoMediaLibrary.web.ts` (refuse permissions
|
|
229
|
+
// + expose MediaType/SortBy constants).
|
|
230
|
+
seamBasenames: ['ExpoMediaLibrary'],
|
|
231
|
+
target: seamPath('expo-media-library'),
|
|
232
|
+
notes:
|
|
233
|
+
'expo-modules-core requireNativeModule(ExpoMediaLibrary) — refuses permissions + exposes MediaType/SortBy enums per upstream ExpoMediaLibrary.web.ts',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
pkg: 'expo-file-system',
|
|
237
|
+
// src/ExpoFileSystem.ts — `requireNativeModule('ExpoFileSystem')`. seam
|
|
238
|
+
// mirrors upstream's `ExpoFileSystem.web.ts` (warn-not-supported stubs).
|
|
239
|
+
seamBasenames: ['ExpoFileSystem'],
|
|
240
|
+
target: seamPath('expo-file-system'),
|
|
241
|
+
notes:
|
|
242
|
+
'expo-modules-core requireNativeModule(ExpoFileSystem) — warn-not-supported stubs per upstream ExpoFileSystem.web.ts; no general browser fs access',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
pkg: 'expo-image-picker',
|
|
246
|
+
// src/ExponentImagePicker.ts — `requireNativeModule('ExponentImagePicker')`.
|
|
247
|
+
// seam routes selection requests through sootsim's engine-owned image
|
|
248
|
+
// picker (`SootSim.bridges.imagePicker.pickFromLibrary`) so the same
|
|
249
|
+
// canvas-rendered iOS picker handles every image-source library.
|
|
250
|
+
seamBasenames: ['ExponentImagePicker'],
|
|
251
|
+
target: seamPath('expo-image-picker'),
|
|
252
|
+
notes:
|
|
253
|
+
"expo-modules-core requireNativeModule(ExponentImagePicker) — routes through engine SootSim.bridges.imagePicker (shell-worker ImagePickerView) instead of upstream web build's DOM <input type=file>",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
pkg: 'expo-camera',
|
|
257
|
+
// upstream splits the native surface across two files:
|
|
258
|
+
// - src/ExpoCamera.ts → requireNativeViewManager('ExpoCamera')
|
|
259
|
+
// - src/ExpoCameraManager.ts → requireNativeModule('ExpoCamera')
|
|
260
|
+
// each basename redirects to a different seam — the view component vs.
|
|
261
|
+
// the imperative module. live preview + capture route through the same
|
|
262
|
+
// engine `camera-stream` module the built-in sootsim Camera app uses,
|
|
263
|
+
// so guest <CameraView> sees the same feed as the OS camera app.
|
|
264
|
+
seamBasenames: ['ExpoCamera'],
|
|
265
|
+
target: seamPath('expo-camera'),
|
|
266
|
+
notes:
|
|
267
|
+
'expo-camera native view manager — host component backed by engine camera-stream (same getUserMedia pipeline as the built-in CameraApp); exposes CameraViewRef methods (takePicture/...) via useImperativeHandle',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
pkg: 'expo-camera',
|
|
271
|
+
seamBasenames: ['ExpoCameraManager'],
|
|
272
|
+
target: seamPath('expo-camera-manager'),
|
|
273
|
+
notes:
|
|
274
|
+
'expo-camera native module — permissions/feature-gates/codecs/Type/FlashMode constants; barcode scanner is noop (no engine scanner integration)',
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
pkg: 'expo-image',
|
|
278
|
+
// upstream splits the native surface across two files:
|
|
279
|
+
// - src/ExpoImage.tsx → requireNativeViewManager('ExpoImage')
|
|
280
|
+
// - src/ImageModule.ts → requireNativeModule('ExpoImage')
|
|
281
|
+
// the view-component seam translates expo-image's prop shape
|
|
282
|
+
// (contentFit, tintColor, source) onto sootsim's existing
|
|
283
|
+
// <sootsim-image> host. no new canvas component is needed — sootsim's
|
|
284
|
+
// image-loader + canvaskit-renderer already handle resizeMode,
|
|
285
|
+
// tintColor, and onLoad/onError lifecycle.
|
|
286
|
+
seamBasenames: ['ExpoImage'],
|
|
287
|
+
target: seamPath('expo-image'),
|
|
288
|
+
notes:
|
|
289
|
+
'expo-image native view — thin prop translator onto sootsim-image host (contentFit→resizeMode, tintColor passthrough); upstream Image.tsx + ImageBackground + useImage resolve from node_modules unchanged',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
pkg: 'expo-image',
|
|
293
|
+
seamBasenames: ['ImageModule'],
|
|
294
|
+
target: seamPath('expo-image-module'),
|
|
295
|
+
notes:
|
|
296
|
+
'expo-image native module — prefetch/loadAsync route through sootsim image-loader; cache controls noop (browser owns HTTP cache); blurhash encode not yet implemented',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
pkg: '@notifee/react-native',
|
|
300
|
+
// src/NotifeeNativeModule.ts — the base class NotifeeApiModule extends.
|
|
301
|
+
// upstream also ships .web.ts (returns `{}` from `native`); both
|
|
302
|
+
// basenames redirect to the same seam — a real implementation that
|
|
303
|
+
// routes `this.native.*` calls through the engine NotificationBus.
|
|
304
|
+
seamBasenames: ['NotifeeNativeModule'],
|
|
305
|
+
target: seamPath('notifee'),
|
|
306
|
+
notes:
|
|
307
|
+
'Notifee NotifeeApiModule base class — routes display/cancel/channel/badge calls through engine NotificationBus; tap events fan out via the shared NotifeeJSEventEmitter so onForegroundEvent listeners fire',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
pkg: 'expo-notifications',
|
|
311
|
+
// five native module spec files, each `requireNativeModule(...)`-shaped.
|
|
312
|
+
// every one redirects to the same `expo-notifications` seam — see
|
|
313
|
+
// `stubs/native-seams/expo-notifications.ts` for the union surface.
|
|
314
|
+
seamBasenames: [
|
|
315
|
+
// src/NotificationsHandlerModule.ts — presentation policy
|
|
316
|
+
'NotificationsHandlerModule',
|
|
317
|
+
// src/NotificationsEmitterModule.ts — last-tap response + listener
|
|
318
|
+
'NotificationsEmitterModule',
|
|
319
|
+
// src/NotificationScheduler.ts — schedule / cancel
|
|
320
|
+
'NotificationScheduler',
|
|
321
|
+
// src/BadgeModule.ts — app badge count
|
|
322
|
+
'BadgeModule',
|
|
323
|
+
// src/ServerRegistrationModule.ts — expo push registration
|
|
324
|
+
'ServerRegistrationModule',
|
|
325
|
+
],
|
|
326
|
+
target: seamPath('expo-notifications'),
|
|
327
|
+
notes:
|
|
328
|
+
'expo-notifications native modules — routes scheduling/badge/installation-id through engine NotificationBus; tap callbacks fan out via the bus event emitters',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
pkg: 'react-native-enriched-markdown',
|
|
332
|
+
seamBasenames: [
|
|
333
|
+
'EnrichedMarkdownTextNativeComponent',
|
|
334
|
+
'EnrichedMarkdownNativeComponent',
|
|
335
|
+
'EnrichedMarkdownTextInputNativeComponent',
|
|
336
|
+
],
|
|
337
|
+
target: seamPath('react-native-enriched-markdown'),
|
|
338
|
+
notes:
|
|
339
|
+
'generated Fabric markdown host components; upstream Streamdown/enriched-markdown JS runs unchanged',
|
|
340
|
+
},
|
|
341
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// the playwright driver writes its browser-host stderr to a tmpdir log, and
|
|
2
|
+
// that log carries the only hardware-WebGL proof the hosted recorder accepts.
|
|
3
|
+
// the driver names the file and the recorder scans for it, so the prefix lives
|
|
4
|
+
// here rather than as a literal in both. it was already renamed once
|
|
5
|
+
// (sootsim- -> rnx-) without the recorder following, which silently failed
|
|
6
|
+
// every hosted job with "run completed without hardware WebGL proof" even when
|
|
7
|
+
// the preview itself uploaded fine.
|
|
8
|
+
export const PLAYWRIGHT_HOST_LOG_PREFIX = 'rnx-playwright-host-'
|
|
9
|
+
|
|
10
|
+
export function playwrightHostLogName(nowMs: number, pid: number): string {
|
|
11
|
+
return `${PLAYWRIGHT_HOST_LOG_PREFIX}${nowMs.toString(36)}-${pid}.log`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isPlaywrightHostLog(name: string): boolean {
|
|
15
|
+
return name.startsWith(PLAYWRIGHT_HOST_LOG_PREFIX) && name.endsWith('.log')
|
|
16
|
+
}
|
package/src/plist.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import plistModule from '@expo/plist'
|
|
2
|
+
|
|
3
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function resolvePlistParser(value: unknown): ((source: string) => unknown) | null {
|
|
8
|
+
if (!isRecord(value)) return null
|
|
9
|
+
const direct = value.parse
|
|
10
|
+
if (typeof direct === 'function') {
|
|
11
|
+
return (source) => Reflect.apply(direct, value, [source])
|
|
12
|
+
}
|
|
13
|
+
const nested = value.default
|
|
14
|
+
if (!isRecord(nested)) return null
|
|
15
|
+
const nestedParse = nested.parse
|
|
16
|
+
return typeof nestedParse === 'function'
|
|
17
|
+
? (source) => Reflect.apply(nestedParse, nested, [source])
|
|
18
|
+
: null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// bun consumes @expo/plist's default export directly, while the compiled commonjs
|
|
22
|
+
// scanner receives the package namespace with that export nested under default.
|
|
23
|
+
const parsePlist = resolvePlistParser(plistModule)
|
|
24
|
+
|
|
25
|
+
export function parsePlistSource(source: string): unknown {
|
|
26
|
+
return parsePlist?.(source) ?? null
|
|
27
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export interface PollUntilOptions {
|
|
2
|
+
timeoutMs: number
|
|
3
|
+
intervalMs?: number
|
|
4
|
+
label: string
|
|
5
|
+
signal?: AbortSignal
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type PollReadOutcome<T> = { kind: 'value'; value: T } | { kind: 'error'; error: unknown }
|
|
9
|
+
|
|
10
|
+
export async function pollUntil<T>(
|
|
11
|
+
read: () => Promise<T>,
|
|
12
|
+
ready: (value: T) => boolean,
|
|
13
|
+
options: PollUntilOptions,
|
|
14
|
+
): Promise<T> {
|
|
15
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
16
|
+
throw new Error('poll timeout must be a positive finite number')
|
|
17
|
+
}
|
|
18
|
+
const intervalMs = options.intervalMs ?? 100
|
|
19
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 0) {
|
|
20
|
+
throw new Error('poll interval must be a non-negative finite number')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const deadline = Date.now() + options.timeoutMs
|
|
24
|
+
let lastError: unknown
|
|
25
|
+
let lastRead: { value: T } | null = null
|
|
26
|
+
// a poll that expires because its predicate kept returning false is the
|
|
27
|
+
// ordinary case, and the error used to name nothing but the label: every one
|
|
28
|
+
// of those sent the reader back to re-run the whole flow just to learn what
|
|
29
|
+
// the state had been. reporting the last value the read produced makes the
|
|
30
|
+
// failure say it. bounded, because a polled value is often a whole evidence
|
|
31
|
+
// snapshot and a failure report is not a data dump.
|
|
32
|
+
const describeLastRead = (): string => {
|
|
33
|
+
if (lastRead === null) {
|
|
34
|
+
if (lastError === undefined) return 'no read settled'
|
|
35
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError)
|
|
36
|
+
return `last read failed: ${message}`
|
|
37
|
+
}
|
|
38
|
+
let serialized: string
|
|
39
|
+
try {
|
|
40
|
+
const seen = new WeakSet()
|
|
41
|
+
serialized =
|
|
42
|
+
JSON.stringify(lastRead.value, (_key, entry: unknown) => {
|
|
43
|
+
if (typeof entry !== 'object' || entry === null) return entry
|
|
44
|
+
if (seen.has(entry)) return '[circular]'
|
|
45
|
+
seen.add(entry)
|
|
46
|
+
return entry
|
|
47
|
+
}) ?? String(lastRead.value)
|
|
48
|
+
} catch {
|
|
49
|
+
return 'last value (unserializable)'
|
|
50
|
+
}
|
|
51
|
+
if (serialized.length <= 300) return `last value ${serialized}`
|
|
52
|
+
return `last value ${serialized.slice(0, 300)}... (${serialized.length} chars)`
|
|
53
|
+
}
|
|
54
|
+
const timeoutError = () =>
|
|
55
|
+
new Error(
|
|
56
|
+
`${options.label} timed out after ${options.timeoutMs}ms; ${describeLastRead()}`,
|
|
57
|
+
{ cause: lastError },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
options.signal?.throwIfAborted()
|
|
61
|
+
let rejectBoundary: (reason: unknown) => void = () => {}
|
|
62
|
+
const boundary = new Promise<never>((_, reject) => {
|
|
63
|
+
rejectBoundary = reject
|
|
64
|
+
})
|
|
65
|
+
const onAbort = () =>
|
|
66
|
+
rejectBoundary(
|
|
67
|
+
options.signal?.reason ??
|
|
68
|
+
new DOMException('The operation was aborted', 'AbortError'),
|
|
69
|
+
)
|
|
70
|
+
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
71
|
+
const deadlineTimer = setTimeout(
|
|
72
|
+
() => rejectBoundary(timeoutError()),
|
|
73
|
+
options.timeoutMs,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
while (true) {
|
|
78
|
+
if (Date.now() >= deadline) throw timeoutError()
|
|
79
|
+
|
|
80
|
+
const outcome = await Promise.race([
|
|
81
|
+
Promise.resolve()
|
|
82
|
+
.then(read)
|
|
83
|
+
.then(
|
|
84
|
+
(value): PollReadOutcome<T> => ({ kind: 'value', value }),
|
|
85
|
+
(error: unknown): PollReadOutcome<T> => ({ kind: 'error', error }),
|
|
86
|
+
),
|
|
87
|
+
boundary,
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
options.signal?.throwIfAborted()
|
|
91
|
+
if (outcome.kind === 'error') {
|
|
92
|
+
lastError = outcome.error
|
|
93
|
+
} else {
|
|
94
|
+
lastRead = { value: outcome.value }
|
|
95
|
+
if (Date.now() >= deadline) throw timeoutError()
|
|
96
|
+
try {
|
|
97
|
+
if (ready(outcome.value)) return outcome.value
|
|
98
|
+
} catch (error) {
|
|
99
|
+
lastError = error
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const remaining = deadline - Date.now()
|
|
104
|
+
if (remaining <= 0) throw timeoutError()
|
|
105
|
+
if (intervalMs > 0) {
|
|
106
|
+
let intervalTimer: ReturnType<typeof setTimeout> | undefined
|
|
107
|
+
try {
|
|
108
|
+
await Promise.race([
|
|
109
|
+
new Promise<void>((resolve) => {
|
|
110
|
+
intervalTimer = setTimeout(resolve, Math.min(intervalMs, remaining))
|
|
111
|
+
}),
|
|
112
|
+
boundary,
|
|
113
|
+
])
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(intervalTimer)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(deadlineTimer)
|
|
121
|
+
options.signal?.removeEventListener('abort', onAbort)
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/profiles.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { electronUserDataDir, ensureRnxHome, profilesDir } from './home-paths'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PROFILE_ID = 'default'
|
|
6
|
+
const PROFILE_INDEX_FILE = 'profiles.json'
|
|
7
|
+
const PROFILE_INDEX_VERSION = 1 as const
|
|
8
|
+
|
|
9
|
+
export interface StorageProfile {
|
|
10
|
+
id: string
|
|
11
|
+
createdAt: string
|
|
12
|
+
updatedAt: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProfileIndex {
|
|
16
|
+
version: 1
|
|
17
|
+
profiles: StorageProfile[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeProfileId(input: string): string {
|
|
21
|
+
const id = input.trim()
|
|
22
|
+
if (!id) throw new Error('profile id is required')
|
|
23
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'profile ids must start with a letter or number and contain only letters, numbers, dot, dash, or underscore',
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
if (id === '.' || id === '..') throw new Error(`invalid profile id: ${id}`)
|
|
29
|
+
return id
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function profileIndexPath(): string {
|
|
33
|
+
return path.join(profilesDir(), PROFILE_INDEX_FILE)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function electronProfilePartitionName(profileId: string): string {
|
|
37
|
+
return `sootsim-profile-${normalizeProfileId(profileId)}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function electronProfilePartition(profileId: string): string {
|
|
41
|
+
return `persist:${electronProfilePartitionName(profileId)}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function electronProfilePartitionDir(profileId: string): string {
|
|
45
|
+
return path.join(
|
|
46
|
+
electronUserDataDir(),
|
|
47
|
+
'Partitions',
|
|
48
|
+
electronProfilePartitionName(profileId),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function playwrightProfileUserDataDir(profileId: string): string {
|
|
53
|
+
return path.join(profilesDir(), 'playwright', normalizeProfileId(profileId))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readIndexRaw(): ProfileIndex {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(
|
|
59
|
+
fs.readFileSync(profileIndexPath(), 'utf8'),
|
|
60
|
+
) as Partial<ProfileIndex> | null
|
|
61
|
+
if (!parsed || parsed.version !== PROFILE_INDEX_VERSION) {
|
|
62
|
+
return { version: PROFILE_INDEX_VERSION, profiles: [] }
|
|
63
|
+
}
|
|
64
|
+
const profiles = Array.isArray(parsed.profiles)
|
|
65
|
+
? parsed.profiles
|
|
66
|
+
.filter(
|
|
67
|
+
(profile): profile is StorageProfile =>
|
|
68
|
+
!!profile &&
|
|
69
|
+
typeof profile.id === 'string' &&
|
|
70
|
+
typeof profile.createdAt === 'string' &&
|
|
71
|
+
typeof profile.updatedAt === 'string',
|
|
72
|
+
)
|
|
73
|
+
.map((profile) => ({
|
|
74
|
+
id: normalizeProfileId(profile.id),
|
|
75
|
+
createdAt: profile.createdAt,
|
|
76
|
+
updatedAt: profile.updatedAt,
|
|
77
|
+
}))
|
|
78
|
+
: []
|
|
79
|
+
return { version: PROFILE_INDEX_VERSION, profiles }
|
|
80
|
+
} catch {
|
|
81
|
+
return { version: PROFILE_INDEX_VERSION, profiles: [] }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sortProfiles(profiles: StorageProfile[]): StorageProfile[] {
|
|
86
|
+
return [...profiles].sort((a, b) => {
|
|
87
|
+
if (a.id === DEFAULT_PROFILE_ID) return -1
|
|
88
|
+
if (b.id === DEFAULT_PROFILE_ID) return 1
|
|
89
|
+
return a.id.localeCompare(b.id)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function withDefault(index: ProfileIndex): ProfileIndex {
|
|
94
|
+
if (index.profiles.some((profile) => profile.id === DEFAULT_PROFILE_ID)) {
|
|
95
|
+
return { ...index, profiles: sortProfiles(index.profiles) }
|
|
96
|
+
}
|
|
97
|
+
const now = new Date().toISOString()
|
|
98
|
+
return {
|
|
99
|
+
version: PROFILE_INDEX_VERSION,
|
|
100
|
+
profiles: sortProfiles([
|
|
101
|
+
{ id: DEFAULT_PROFILE_ID, createdAt: now, updatedAt: now },
|
|
102
|
+
...index.profiles,
|
|
103
|
+
]),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeIndex(index: ProfileIndex): ProfileIndex {
|
|
108
|
+
ensureRnxHome()
|
|
109
|
+
const next = withDefault(index)
|
|
110
|
+
const tmp = `${profileIndexPath()}.tmp`
|
|
111
|
+
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
|
|
112
|
+
fs.renameSync(tmp, profileIndexPath())
|
|
113
|
+
return next
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function listProfiles(): StorageProfile[] {
|
|
117
|
+
return writeIndex(readIndexRaw()).profiles
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getProfile(id: string): StorageProfile | null {
|
|
121
|
+
const normalized = normalizeProfileId(id)
|
|
122
|
+
return listProfiles().find((profile) => profile.id === normalized) ?? null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function ensureProfile(id = DEFAULT_PROFILE_ID): StorageProfile {
|
|
126
|
+
const normalized = normalizeProfileId(id)
|
|
127
|
+
const existing = getProfile(normalized)
|
|
128
|
+
if (existing) return existing
|
|
129
|
+
return createProfile(normalized)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createProfile(id: string): StorageProfile {
|
|
133
|
+
const normalized = normalizeProfileId(id)
|
|
134
|
+
const index = withDefault(readIndexRaw())
|
|
135
|
+
if (index.profiles.some((profile) => profile.id === normalized)) {
|
|
136
|
+
throw new Error(`profile already exists: ${normalized}`)
|
|
137
|
+
}
|
|
138
|
+
const now = new Date().toISOString()
|
|
139
|
+
const profile = { id: normalized, createdAt: now, updatedAt: now }
|
|
140
|
+
writeIndex({ version: PROFILE_INDEX_VERSION, profiles: [...index.profiles, profile] })
|
|
141
|
+
return profile
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function nextGeneratedProfileId(): string {
|
|
145
|
+
const existing = new Set(listProfiles().map((profile) => profile.id))
|
|
146
|
+
for (let i = 1; i < 10_000; i++) {
|
|
147
|
+
const id = `profile-${i}`
|
|
148
|
+
if (!existing.has(id)) return id
|
|
149
|
+
}
|
|
150
|
+
throw new Error('could not allocate a new profile id')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function deleteProfile(id: string): StorageProfile {
|
|
154
|
+
const normalized = normalizeProfileId(id)
|
|
155
|
+
if (normalized === DEFAULT_PROFILE_ID) {
|
|
156
|
+
throw new Error('the default profile cannot be deleted; clear it instead')
|
|
157
|
+
}
|
|
158
|
+
const index = withDefault(readIndexRaw())
|
|
159
|
+
const profile = index.profiles.find((entry) => entry.id === normalized)
|
|
160
|
+
if (!profile) throw new Error(`profile not found: ${normalized}`)
|
|
161
|
+
writeIndex({
|
|
162
|
+
version: PROFILE_INDEX_VERSION,
|
|
163
|
+
profiles: index.profiles.filter((entry) => entry.id !== normalized),
|
|
164
|
+
})
|
|
165
|
+
clearProfileStorage(normalized)
|
|
166
|
+
return profile
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function clearProfileStorage(id: string): void {
|
|
170
|
+
const normalized = normalizeProfileId(id)
|
|
171
|
+
for (const dir of [
|
|
172
|
+
electronProfilePartitionDir(normalized),
|
|
173
|
+
playwrightProfileUserDataDir(normalized),
|
|
174
|
+
]) {
|
|
175
|
+
try {
|
|
176
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
}
|