rnxsim 0.1.416 → 0.1.418

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.
Files changed (59) hide show
  1. package/cli/cloud-client.ts +104 -32
  2. package/cli/cloud-dispatch.ts +38 -4
  3. package/cli/commands/inspect/actions.ts +313 -385
  4. package/cli/commands/inspect/core.ts +6 -0
  5. package/cli/commands/inspect/resolve-target.ts +2 -3
  6. package/cli/commands/inspect/settle.ts +10 -0
  7. package/cli/commands/inspect.ts +31 -48
  8. package/cli/commands/platform.ts +41 -10
  9. package/cli/outbound-endpoints.ts +1 -1
  10. package/cli/shell-init.ts +1 -1
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +1 -1
  22. package/dist-lib/bridge-contract-input.mjs +1 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/detox/index.cjs +1 -1
  32. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  33. package/dist-lib/home-paths.cjs +1 -1
  34. package/dist-lib/host/bridge-host.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  38. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  39. package/dist-lib/host/websocket-proxy.cjs +1 -1
  40. package/dist-lib/index.cjs +626 -749
  41. package/dist-lib/jump-to-source-babel.cjs +1 -1
  42. package/dist-lib/menu.cjs +1 -1
  43. package/dist-lib/menu.mjs +1 -1
  44. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  46. package/dist-lib/metro-production-bundle.cjs +1 -1
  47. package/dist-lib/metro-production-bundle.mjs +1 -1
  48. package/dist-lib/metro.cjs +1 -1
  49. package/dist-lib/profiles.cjs +1 -1
  50. package/dist-lib/public-brand.cjs +1 -1
  51. package/dist-lib/react-native-host-modules.cjs +1 -1
  52. package/dist-lib/react-native-host-modules.mjs +1 -1
  53. package/dist-lib/render-mode.cjs +1 -1
  54. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  55. package/dist-lib/sdk.cjs +1206 -1342
  56. package/dist-lib/sdk.mjs +1206 -1342
  57. package/dist-lib/skills.cjs +57 -1
  58. package/dist-lib/vite.cjs +1 -1
  59. package/package.json +1 -1
@@ -1,19 +1,27 @@
1
1
  import { createHash, randomUUID } from 'node:crypto'
2
- import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs'
2
+ import { closeSync, fstatSync, openSync, readdirSync, readFileSync } from 'node:fs'
3
3
  import { dirname, extname, join, resolve } from 'node:path'
4
4
  import {
5
5
  embedMetroAssetFiles,
6
- type MetroAssetVariant,
6
+ metroAssetScaleFilename,
7
+ metroIosAssetDestRelatives,
8
+ resolveMetroIosAssetDestFile,
7
9
  } from '../../contrast-bundler/src/metroAssetArtifact'
8
10
  import {
9
11
  buildRnxCloudArtifact,
10
12
  type RnxCloudArtifactReceipt,
11
13
  } from '../../contrast-bundler/src/metroCloudArtifact'
14
+ import { inferMetroModuleIdentity } from '../../sootsim-engine/src/metro-fingerprint'
15
+ import { loadMetroFingerprintRegistry } from '../../sootsim-engine/src/metro-fingerprint-registry-client'
12
16
  import {
13
17
  isRnxCloudCommandType,
14
18
  RNX_CLOUD_MAX_ARTIFACT_BYTES,
15
19
  type RnxCloudBoxCreateReceipt,
16
20
  } from '../src/cloud-contract'
21
+ import {
22
+ RNX_METRO_MODULE_IDENTITY_VERSION,
23
+ type RNXMetroModuleIdentityMetadata,
24
+ } from '../src/metro-production-bundle'
17
25
  import { rnxPublicBrand } from '../src/public-brand'
18
26
  import {
19
27
  createCloudSession,
@@ -74,16 +82,16 @@ function resolveCloudOrigin(value?: string): string {
74
82
  function readImmutableBundleSource(bundlePath: string): string {
75
83
  const filepath = resolve(bundlePath)
76
84
  if (extname(filepath) !== '.js') {
77
- throw new Error('rnx ios --cloud requires one local Metro .js bundle')
85
+ throw new Error('rnx ios --nano requires one local Metro .js bundle')
78
86
  }
79
87
  const descriptor = openSync(filepath, 'r')
80
88
  try {
81
89
  const before = fstatSync(descriptor, { bigint: true })
82
90
  if (!before.isFile()) {
83
- throw new Error('rnx ios --cloud bundle must be a regular local file')
91
+ throw new Error('rnx ios --nano bundle must be a regular local file')
84
92
  }
85
93
  if (before.size <= 0n) {
86
- throw new Error('rnx ios --cloud bundle is empty')
94
+ throw new Error('rnx ios --nano bundle is empty')
87
95
  }
88
96
  const bytes = readFileSync(descriptor)
89
97
  const after = fstatSync(descriptor, { bigint: true })
@@ -94,16 +102,16 @@ function readImmutableBundleSource(bundlePath: string): string {
94
102
  before.mtimeNs !== after.mtimeNs ||
95
103
  bytes.length !== Number(before.size)
96
104
  ) {
97
- throw new Error('rnx ios --cloud bundle changed while it was being read')
105
+ throw new Error('rnx ios --nano bundle changed while it was being read')
98
106
  }
99
107
  let source: string
100
108
  try {
101
109
  source = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
102
110
  } catch {
103
- throw new Error('rnx ios --cloud bundle must be valid UTF-8 JavaScript')
111
+ throw new Error('rnx ios --nano bundle must be valid UTF-8 JavaScript')
104
112
  }
105
113
  if (source.includes('\0') || !source.includes('__d(') || !source.includes('__r(')) {
106
- throw new Error('rnx ios --cloud requires a Metro JavaScript bundle')
114
+ throw new Error('rnx ios --nano requires a Metro JavaScript bundle')
107
115
  }
108
116
  return source
109
117
  } finally {
@@ -111,46 +119,110 @@ function readImmutableBundleSource(bundlePath: string): string {
111
119
  }
112
120
  }
113
121
 
114
- // `react-native bundle --assets-dest <dir>` and `expo export:embed` write every
115
- // scale variant with getAssetDestPathIOS: the descriptor's httpServerLocation
116
- // without its leading slash, each `../` collapsed to `_`, then the asset name
117
- // with its scale suffix. reading that layout is what lets an ordinary Metro
118
- // build reach the cloud with no per-app script.
119
- function metroAssetDestPath(assetsPath: string, variant: MetroAssetVariant): string {
120
- const { descriptor, scale } = variant
121
- const suffix = scale === 1 ? '' : `@${scale}x`
122
- return join(
123
- assetsPath,
124
- descriptor.httpServerLocation.slice(1).replace(/\.\.\//g, '_'),
125
- `${descriptor.name}${suffix}.${descriptor.type}`,
126
- )
122
+ // `react-native bundle --assets-dest` dest paths are not always the JS
123
+ // descriptor's httpServerLocation with `../` replaced by `_`. Metro 0.83 does
124
+ // that (`__node_modules`); Metro 0.84 often dests `node_modules` and keeps the
125
+ // project-root-relative app path. resolveMetroIosAssetDestFile matches both.
126
+ function listMetroAssetDestFiles(assetsPath: string): string[] {
127
+ const files: string[] = []
128
+ const walk = (dir: string, prefix: string) => {
129
+ let entries
130
+ try {
131
+ entries = readdirSync(dir, { withFileTypes: true })
132
+ } catch {
133
+ return
134
+ }
135
+ for (const entry of entries) {
136
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name
137
+ if (entry.isDirectory()) walk(join(dir, entry.name), rel)
138
+ else if (entry.isFile()) files.push(rel)
139
+ }
140
+ }
141
+ walk(assetsPath, '')
142
+ return files
143
+ }
144
+
145
+ async function inferCloudModuleIdentity(
146
+ source: string,
147
+ ): Promise<RNXMetroModuleIdentityMetadata> {
148
+ let receipt
149
+ try {
150
+ receipt = await loadMetroFingerprintRegistry()
151
+ } catch (error) {
152
+ throw new Error(
153
+ `rnx Cloud could not load the Metro fingerprint registry needed to identify this bundle: ${
154
+ error instanceof Error ? error.message : String(error)
155
+ }. Build it through the published Metro plugin: withRNX(config, { productionBundles: true }) for the production graph, or withRNX(config, { developmentBundles: true }) for the development graph.`,
156
+ )
157
+ }
158
+ try {
159
+ const inferred = await inferMetroModuleIdentity({
160
+ source,
161
+ registry: receipt.registry,
162
+ registryIntegrity: receipt.integrity,
163
+ })
164
+ return {
165
+ version: RNX_METRO_MODULE_IDENTITY_VERSION,
166
+ identitySource: 'contrast-bundler',
167
+ modulePaths: inferred.modulePaths,
168
+ logicalSpecifiers: {},
169
+ }
170
+ } catch (error) {
171
+ throw new Error(
172
+ `rnx Cloud could not infer module identity for this Metro bundle: ${
173
+ error instanceof Error ? error.message : String(error)
174
+ }. Build it through the published Metro plugin: withRNX(config, { productionBundles: true }) for the production graph, or withRNX(config, { developmentBundles: true }) for the development graph.`,
175
+ )
176
+ }
127
177
  }
128
178
 
129
179
  /**
130
180
  * turns the Metro production bundle a caller built into the immutable artifact
131
181
  * the runner executes: every asset scale is read from the asset graph and
132
182
  * embedded, module identity is normalized, and the JavaScript is minified.
183
+ * an ordinary unannotated Metro production bundle is identified through the
184
+ * published fingerprint registry; a withRNX footer remains the fast path.
133
185
  */
134
186
  async function produceCloudArtifact(
135
187
  bundlePath: string,
136
188
  assetsPath: string,
137
189
  ): Promise<{ bytes: Buffer; sha256: string; receipt: RnxCloudArtifactReceipt }> {
138
190
  const source = readImmutableBundleSource(bundlePath)
191
+ const destFiles = listMetroAssetDestFiles(assetsPath)
139
192
  const embedded = await embedMetroAssetFiles(source, async (variant) => {
140
- const file = metroAssetDestPath(assetsPath, variant)
141
- try {
142
- return readFileSync(file)
143
- } catch {
144
- throw new Error(
145
- `rnx ios --cloud could not read ${file} for ${variant.descriptor.name}.${variant.descriptor.type} at scale ${variant.scale}. Build the bundle with --assets-dest, or pass --assets <dir>.`,
146
- )
147
- }
193
+ const relative = resolveMetroIosAssetDestFile(
194
+ destFiles,
195
+ variant.descriptor,
196
+ variant.scale,
197
+ )
198
+ if (relative) return readFileSync(join(assetsPath, relative))
199
+ // iOS copies only @1x..@3x, so a declared scale with no file is ordinary
200
+ // and the producer narrows the descriptor. an asset with no file at any
201
+ // scale is the real failure, and this is where the dest path a customer
202
+ // should look for is still known.
203
+ const written = variant.descriptor.scales.some((scale) =>
204
+ resolveMetroIosAssetDestFile(destFiles, variant.descriptor, scale),
205
+ )
206
+ if (written) return null
207
+ const filename = metroAssetScaleFilename(
208
+ variant.descriptor.name,
209
+ variant.descriptor.type,
210
+ variant.scale,
211
+ )
212
+ const classic =
213
+ metroIosAssetDestRelatives(variant.descriptor.httpServerLocation, filename)[0] ??
214
+ filename
215
+ throw new Error(
216
+ `rnx ios --nano could not read ${join(assetsPath, classic)} for ${variant.descriptor.name}.${variant.descriptor.type} at scale ${variant.scale}. Build the bundle with --assets-dest, or pass --assets <dir>.`,
217
+ )
218
+ })
219
+ const artifact = await buildRnxCloudArtifact(embedded.code, {
220
+ inferModuleIdentity: inferCloudModuleIdentity,
148
221
  })
149
- const artifact = await buildRnxCloudArtifact(embedded.code)
150
222
  const bytes = Buffer.from(artifact.code, 'utf8')
151
223
  if (bytes.length > MAX_RNX_CLOUD_BUNDLE_BYTES) {
152
224
  throw new Error(
153
- `rnx ios --cloud artifact is ${bytes.length} bytes and exceeds the ${MAX_RNX_CLOUD_BUNDLE_BYTES}-byte limit`,
225
+ `rnx ios --nano artifact is ${bytes.length} bytes and exceeds the ${MAX_RNX_CLOUD_BUNDLE_BYTES}-byte limit`,
154
226
  )
155
227
  }
156
228
  return {
@@ -219,7 +291,7 @@ function readCreateResponse(
219
291
  }
220
292
  }
221
293
 
222
- // `rnx ios --cloud <bundle>` creates a nano box around the artifact with its
294
+ // `rnx ios --nano <bundle>` creates a nano box around the artifact with its
223
295
  // first simulator in one request, and the shell then targets that box
224
296
  export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
225
297
  receipt: RnxCloudBoxCreateReceipt
@@ -38,6 +38,39 @@ function firstVerb(positional: string[]): string | null {
38
38
  return positional.find((value) => !value.startsWith('-')) ?? null
39
39
  }
40
40
 
41
+ const CLOUD_DO_VERBS = new Set([
42
+ 'tap',
43
+ 'tap-id',
44
+ 'tap-text',
45
+ 'tap-best',
46
+ 'double-tap',
47
+ 'long-press',
48
+ 'sleep',
49
+ 'settle',
50
+ ])
51
+
52
+ function doChainVerbs(positional: string[]): string[] {
53
+ const verbs: string[] = []
54
+ let expectingVerb = true
55
+ for (const argument of positional) {
56
+ if (argument === '--then') {
57
+ expectingVerb = true
58
+ continue
59
+ }
60
+ if (expectingVerb && !argument.startsWith('-')) {
61
+ verbs.push(argument)
62
+ expectingVerb = false
63
+ }
64
+ }
65
+ return verbs
66
+ }
67
+
68
+ function supportsCloudDo(positional: string[]): boolean {
69
+ const verbs = doChainVerbs(positional)
70
+ if (verbs.length === 0) return true
71
+ return verbs.every((verb) => CLOUD_DO_VERBS.has(verb))
72
+ }
73
+
41
74
  function supportsCloudRead(
42
75
  command: string,
43
76
  args: string[],
@@ -77,7 +110,8 @@ function supportsCloudRead(
77
110
  ].includes(arg),
78
111
  )
79
112
  }
80
- if (command === 'do' || command === 'reset') return true
113
+ if (command === 'do') return supportsCloudDo(positional)
114
+ if (command === 'reset') return true
81
115
  return false
82
116
  }
83
117
 
@@ -95,10 +129,10 @@ export async function dispatchCloudBoundary({
95
129
  command,
96
130
  args,
97
131
  }: CloudBoundaryInvocation): Promise<CloudBoundaryResult> {
98
- if (command === 'ios' && args.includes('--cloud')) return { handled: false }
99
- if (command === 'android' && args.includes('--cloud')) {
132
+ if (command === 'ios' && args.includes('--nano')) return { handled: false }
133
+ if (command === 'android' && args.includes('--nano')) {
100
134
  console.error(
101
- ' rnx android --cloud is not available; RNX Cloud currently supports iOS',
135
+ ' rnx android --nano is not available; RNX Cloud currently supports iOS',
102
136
  )
103
137
  return { handled: true, code: 1 }
104
138
  }