rnxsim 0.1.431 → 0.1.433

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 (78) hide show
  1. package/README.md +20 -50
  2. package/cli/auth.ts +33 -3
  3. package/cli/cloud-client.ts +524 -88
  4. package/cli/cloud-dispatch.ts +5 -10
  5. package/cli/cloud-session.ts +5 -5
  6. package/cli/commands/auth.ts +13 -2
  7. package/cli/commands/box.ts +189 -20
  8. package/cli/commands/control.ts +40 -13
  9. package/cli/commands/device.ts +1 -1
  10. package/cli/commands/flow.ts +189 -43
  11. package/cli/commands/inspect/get-layout.ts +1 -1
  12. package/cli/commands/inspect.ts +8 -1
  13. package/cli/commands/login.ts +79 -14
  14. package/cli/commands/maestro-generate.ts +70 -54
  15. package/cli/commands/maestro.ts +69 -20
  16. package/cli/commands/platform.ts +30 -32
  17. package/cli/commands/screenshot.ts +3 -2
  18. package/cli/commands/upload.ts +28 -10
  19. package/cli/help-core.ts +11 -1
  20. package/cli/outbound-endpoints.ts +27 -5
  21. package/cli/parse-args.ts +17 -0
  22. package/cli/shell-init.ts +4 -4
  23. package/cli/ws-bridge.ts +109 -62
  24. package/dist-lib/agent-daemon-client.cjs +1 -1
  25. package/dist-lib/agent-events.cjs +1 -1
  26. package/dist-lib/agent-identity.cjs +1 -1
  27. package/dist-lib/agent-sessions.cjs +1 -1
  28. package/dist-lib/attached-projects.cjs +1 -1
  29. package/dist-lib/auth/shared-session.cjs +17 -4
  30. package/dist-lib/backend-origin.cjs +1 -1
  31. package/dist-lib/beta.cjs +1 -1
  32. package/dist-lib/beta.mjs +1 -1
  33. package/dist-lib/bridge-constants.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.mjs +1 -1
  36. package/dist-lib/bridge-contract.cjs +1 -1
  37. package/dist-lib/bridge-contract.mjs +1 -1
  38. package/dist-lib/capture-contract.cjs +1 -1
  39. package/dist-lib/capture-contract.mjs +1 -1
  40. package/dist-lib/cli-constants.cjs +1 -1
  41. package/dist-lib/cloud-contract.cjs +1 -1
  42. package/dist-lib/cloud-contract.mjs +1 -1
  43. package/dist-lib/cloud.cjs +108 -93
  44. package/dist-lib/cloud.mjs +106 -91
  45. package/dist-lib/config.cjs +1 -1
  46. package/dist-lib/detox/index.cjs +1 -1
  47. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  48. package/dist-lib/home-paths.cjs +1 -1
  49. package/dist-lib/host/bridge-host.cjs +1 -1
  50. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  51. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  52. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  53. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  54. package/dist-lib/host/websocket-proxy.cjs +1 -1
  55. package/dist-lib/index.cjs +58 -139
  56. package/dist-lib/jump-to-source-babel.cjs +1 -1
  57. package/dist-lib/menu.cjs +1 -1
  58. package/dist-lib/menu.mjs +1 -1
  59. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  60. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  61. package/dist-lib/metro-production-bundle.cjs +1 -1
  62. package/dist-lib/metro-production-bundle.mjs +1 -1
  63. package/dist-lib/metro.cjs +33 -212
  64. package/dist-lib/profiles.cjs +1 -1
  65. package/dist-lib/public-brand.cjs +1 -1
  66. package/dist-lib/react-native-host-modules.cjs +1 -1
  67. package/dist-lib/react-native-host-modules.mjs +1 -1
  68. package/dist-lib/render-mode.cjs +1 -1
  69. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  70. package/dist-lib/sdk.cjs +1 -1
  71. package/dist-lib/sdk.mjs +1 -1
  72. package/dist-lib/skills.cjs +436 -351
  73. package/dist-lib/vite.cjs +1 -1
  74. package/package.json +2 -1
  75. package/src/auth/shared-session.ts +31 -0
  76. package/src/cloud.ts +83 -113
  77. package/src/metro-plugin.ts +30 -113
  78. package/src/vite-plugin.ts +11 -2
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.431 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.433 | (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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.431",
3
+ "version": "0.1.433",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for rnx.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -234,6 +234,7 @@
234
234
  "esbuild": "^0.25.0",
235
235
  "jiti": "2.7.0",
236
236
  "playwright-core": "1.61.0",
237
+ "terser": "^5.49.0",
237
238
  "ws": "^8.18.0"
238
239
  },
239
240
  "peerDependencies": {
@@ -22,6 +22,8 @@ export type SharedDesktopAuthUser = {
22
22
  email?: string
23
23
  image?: string
24
24
  githubUsername?: string
25
+ /** the person's own account; every team they belong to is in `teams` */
26
+ personalAccountId?: string
25
27
  teams?: SharedDesktopAuthTeamSummary[]
26
28
  isAdmin?: boolean
27
29
  }
@@ -34,6 +36,24 @@ export type SharedDesktopAuthSession = {
34
36
  source: 'cli' | 'electron' | 'browser' | 'unknown'
35
37
  updatedAt: string
36
38
  validatedAt?: string
39
+ /**
40
+ * the account this sign-in bills cloud work to. a session token names the
41
+ * person, not an account, so the CLI chooses one at `rnx login` and names it
42
+ * on every request that opens a paid session.
43
+ */
44
+ accountId?: string
45
+ }
46
+
47
+ /** the accounts a signed-in person can bill: their own, then their teams. */
48
+ export function sharedDesktopAuthAccounts(
49
+ user: SharedDesktopAuthUser | null | undefined,
50
+ ): { id: string; name: string }[] {
51
+ return [
52
+ ...(user?.personalAccountId
53
+ ? [{ id: user.personalAccountId, name: 'Personal' }]
54
+ : []),
55
+ ...(user?.teams ?? []).map((team) => ({ id: team.id, name: team.name })),
56
+ ]
37
57
  }
38
58
 
39
59
  const SESSION_VERSION = 1 as const
@@ -65,6 +85,10 @@ function normalizeUser(input: unknown): SharedDesktopAuthUser | null {
65
85
  image: typeof value.image === 'string' ? value.image : undefined,
66
86
  githubUsername:
67
87
  typeof value.githubUsername === 'string' ? value.githubUsername : undefined,
88
+ personalAccountId:
89
+ typeof value.personalAccountId === 'string' && value.personalAccountId.trim()
90
+ ? value.personalAccountId.trim()
91
+ : undefined,
68
92
  isAdmin: value.isAdmin === true,
69
93
  teams: Array.isArray(value.teams)
70
94
  ? value.teams
@@ -108,6 +132,10 @@ function normalizeSession(input: unknown): SharedDesktopAuthSession | null {
108
132
  typeof value.validatedAt === 'string' && value.validatedAt
109
133
  ? value.validatedAt
110
134
  : undefined
135
+ const accountId =
136
+ typeof value.accountId === 'string' && value.accountId.trim()
137
+ ? value.accountId.trim()
138
+ : undefined
111
139
 
112
140
  return {
113
141
  version: SESSION_VERSION,
@@ -117,6 +145,7 @@ function normalizeSession(input: unknown): SharedDesktopAuthSession | null {
117
145
  source,
118
146
  updatedAt,
119
147
  validatedAt,
148
+ accountId,
120
149
  }
121
150
  }
122
151
 
@@ -153,6 +182,7 @@ export function writeSharedDesktopAuthSession(
153
182
  source: session.source,
154
183
  updatedAt: session.updatedAt || new Date().toISOString(),
155
184
  validatedAt: session.validatedAt,
185
+ accountId: session.accountId?.trim() || undefined,
156
186
  }
157
187
  writeFileSync(filepath, JSON.stringify(normalized, null, 2) + '\n')
158
188
  try {
@@ -197,6 +227,7 @@ export async function refreshSharedDesktopAuthSession(
197
227
  origin,
198
228
  source: current.source,
199
229
  validatedAt: new Date().toISOString(),
230
+ accountId: current.accountId,
200
231
  })
201
232
  } catch {
202
233
  return current
package/src/cloud.ts CHANGED
@@ -1,30 +1,37 @@
1
- // typed RNX Cloud nano client for agents. the CLI produces artifact bytes
1
+ // typed Remote Simulators client for agents. the CLI produces artifact bytes
2
2
  // (local file or a fetched URL); this module talks to the service in those
3
3
  // bytes only. command types and receipts are the same ones the CLI already
4
4
  // sends. do not invent a second vocabulary.
5
+ //
6
+ // one call gives you one remote simulator. it runs on a nano box today, which
7
+ // is why the session below carries a box id and the create request asks for a
8
+ // size, but that is the service's business: nothing box-shaped reaches the
9
+ // public surface.
5
10
 
6
11
  import { isLoopbackHost } from './backend-origin'
12
+ import { isRnxScreenCapture, type RnxScreenCapture } from './capture-contract'
7
13
  import {
8
14
  isRnxCloudCommandType,
9
15
  RNX_CLOUD_COMMAND_TYPES,
10
16
  RNX_CLOUD_MAX_ARTIFACT_BYTES,
11
17
  type RnxCloudBoxCreateReceipt,
12
18
  type RnxCloudClaim,
19
+ type RnxCloudCreateReceipt,
13
20
  } from './cloud-contract'
14
21
  import { rnxPublicBrand } from './public-brand'
15
22
 
16
23
  export { isRnxCloudCommandType, RNX_CLOUD_COMMAND_TYPES, RNX_CLOUD_MAX_ARTIFACT_BYTES }
17
- export type { RnxCloudBoxCreateReceipt, RnxCloudClaim }
24
+ export type { RnxCloudClaim, RnxCloudCreateReceipt, RnxScreenCapture }
18
25
 
19
26
  const DEFAULT_DEVICE = 'iphone-16'
20
27
  const DEFAULT_WAIT_READY_MS = 20_000
21
28
 
22
- export type NanoCommand = {
29
+ export type RemoteCommand = {
23
30
  type: (typeof RNX_CLOUD_COMMAND_TYPES)[number]
24
31
  [key: string]: unknown
25
32
  }
26
33
 
27
- export interface CreateNanoBoxInput {
34
+ export interface CreateRemoteSimulatorInput {
28
35
  artifact: Uint8Array
29
36
  authorization: string
30
37
  origin?: string
@@ -32,20 +39,33 @@ export interface CreateNanoBoxInput {
32
39
  sha256?: string
33
40
  }
34
41
 
35
- export interface AddNanoSimulatorInput {
36
- device?: string
37
- storageFrom?: string | null
42
+ // the simulator-driving verbs. the same vocabulary the CLI drives, one for
43
+ // one: `rnx describe`, `rnx do tap`, `rnx wait selector`.
44
+ export interface RnxCommands {
45
+ command(
46
+ command: { type: string; [key: string]: unknown },
47
+ options?: { timeoutMs?: number },
48
+ ): Promise<unknown>
49
+ describe(): Promise<unknown>
50
+ do(command: { type: string; [key: string]: unknown }): Promise<unknown>
51
+ wait(
52
+ condition: 'ready' | { type: 'selector'; selector: string },
53
+ options?: { maxMs?: number },
54
+ ): Promise<unknown>
55
+ // a `data:image/png;base64,` url, assembled by the service from the frame
56
+ screenshot(options?: { layers?: 'full' | 'tenant' | 'shell' }): Promise<string>
57
+ capture(): Promise<RnxScreenCapture>
58
+ reset(): Promise<unknown>
38
59
  }
39
60
 
40
- interface NanoSession {
61
+ interface RemoteSimulatorSession {
41
62
  origin: string
42
- authorization: string
43
63
  boxId: string
44
64
  boxToken: string
45
65
  simId: string
46
66
  simulatorToken: string
47
67
  claim: RnxCloudClaim
48
- artifact: RnxCloudBoxCreateReceipt['artifact']
68
+ artifact: RnxCloudCreateReceipt['artifact']
49
69
  }
50
70
 
51
71
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -221,7 +241,7 @@ function readBoxCreate(
221
241
  }
222
242
  }
223
243
 
224
- function cloudCommand(command: { type: string; [key: string]: unknown }): NanoCommand {
244
+ function cloudCommand(command: { type: string; [key: string]: unknown }): RemoteCommand {
225
245
  const clean: Record<string, unknown> = {}
226
246
  for (const [key, value] of Object.entries(command)) {
227
247
  if (key !== 'id' && key !== 'simId') clean[key] = value
@@ -232,15 +252,50 @@ function cloudCommand(command: { type: string; [key: string]: unknown }): NanoCo
232
252
  return { ...clean, type: command.type }
233
253
  }
234
254
 
235
- export class NanoBox {
236
- private session: NanoSession
255
+ export class RemoteSimulator {
256
+ private session: RemoteSimulatorSession
237
257
 
238
- constructor(session: NanoSession) {
239
- this.session = session
240
- }
258
+ // driving verbs live here so the client reads the way the CLI does. bound to
259
+ // this simulator at construction, and every verb reads the session live, so a
260
+ // confirmed claim reaches commands issued after it.
261
+ readonly rnx: RnxCommands
241
262
 
242
- get boxId(): string {
243
- return this.session.boxId
263
+ constructor(session: RemoteSimulatorSession) {
264
+ this.session = session
265
+ this.rnx = {
266
+ command: (command, options) => this.sendCommand(command, options),
267
+ describe: () => this.sendCommand({ type: 'tree' }),
268
+ do: (command) => this.sendCommand(command),
269
+ wait: (condition, options) => {
270
+ const timeoutMs = options?.maxMs ?? DEFAULT_WAIT_READY_MS
271
+ const waitCondition = condition === 'ready' ? { type: 'ready' } : condition
272
+ return this.sendCommand(
273
+ {
274
+ type: 'waitFor',
275
+ waitForOptions: { condition: waitCondition, timeoutMs },
276
+ },
277
+ { timeoutMs: timeoutMs + 1_000 },
278
+ )
279
+ },
280
+ screenshot: async (options) => {
281
+ const result = await this.sendCommand({
282
+ type: 'screenshot',
283
+ layers: options?.layers ?? 'tenant',
284
+ })
285
+ if (typeof result !== 'string' || !result.startsWith('data:image/')) {
286
+ throw new Error('RNX Cloud screenshot did not return an image data url')
287
+ }
288
+ return result
289
+ },
290
+ capture: async () => {
291
+ const result = await this.sendCommand({ type: 'capture' })
292
+ if (!isRnxScreenCapture(result)) {
293
+ throw new Error('RNX Cloud capture returned an invalid screen capture')
294
+ }
295
+ return result
296
+ },
297
+ reset: () => this.sendCommand({ type: 'reset' }),
298
+ }
244
299
  }
245
300
 
246
301
  get simId(): string {
@@ -251,20 +306,15 @@ export class NanoBox {
251
306
  return this.session.claim
252
307
  }
253
308
 
254
- get receipt(): RnxCloudBoxCreateReceipt {
309
+ get receipt(): RnxCloudCreateReceipt {
255
310
  return {
256
- boxId: this.session.boxId,
257
- size: 'nano',
311
+ simId: this.session.simId,
258
312
  artifact: this.session.artifact,
259
- simulator: {
260
- simId: this.session.simId,
261
- artifact: this.session.artifact,
262
- claim: this.session.claim,
263
- },
313
+ claim: this.session.claim,
264
314
  }
265
315
  }
266
316
 
267
- async command(
317
+ private async sendCommand(
268
318
  command: { type: string; [key: string]: unknown },
269
319
  options?: { timeoutMs?: number },
270
320
  ): Promise<unknown> {
@@ -314,44 +364,6 @@ export class NanoBox {
314
364
  return parsed.result
315
365
  }
316
366
 
317
- describe(): Promise<unknown> {
318
- return this.command({ type: 'tree' })
319
- }
320
-
321
- do(command: { type: string; [key: string]: unknown }): Promise<unknown> {
322
- return this.command(command)
323
- }
324
-
325
- wait(
326
- condition: 'ready' | { type: 'selector'; selector: string },
327
- options?: { maxMs?: number },
328
- ): Promise<unknown> {
329
- const timeoutMs = options?.maxMs ?? DEFAULT_WAIT_READY_MS
330
- const waitCondition = condition === 'ready' ? { type: 'ready' } : condition
331
- return this.command(
332
- {
333
- type: 'waitFor',
334
- waitForOptions: { condition: waitCondition, timeoutMs },
335
- },
336
- { timeoutMs: timeoutMs + 1_000 },
337
- )
338
- }
339
-
340
- screenshot(options?: { layers?: 'full' | 'tenant' | 'shell' }): Promise<unknown> {
341
- return this.command({
342
- type: 'screenshot',
343
- layers: options?.layers ?? 'tenant',
344
- })
345
- }
346
-
347
- capture(): Promise<unknown> {
348
- return this.command({ type: 'capture' })
349
- }
350
-
351
- reset(): Promise<unknown> {
352
- return this.command({ type: 'reset' })
353
- }
354
-
355
367
  async confirm(): Promise<RnxCloudClaim> {
356
368
  const response = await fetch(
357
369
  `${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}/claim`,
@@ -380,49 +392,6 @@ export class NanoBox {
380
392
  return claim
381
393
  }
382
394
 
383
- async addSimulator(input: AddNanoSimulatorInput = {}): Promise<NanoBox> {
384
- const device = input.device?.trim() || DEFAULT_DEVICE
385
- const response = await fetch(
386
- `${this.session.origin}/v1/boxes/${encodeURIComponent(this.session.boxId)}/simulators`,
387
- {
388
- method: 'POST',
389
- headers: {
390
- authorization: `Bearer ${this.session.boxToken}`,
391
- 'x-rnx-account-authorization': this.session.authorization,
392
- 'content-type': 'application/json',
393
- },
394
- body: JSON.stringify({
395
- device,
396
- ...(input.storageFrom === undefined ? {} : { storageFrom: input.storageFrom }),
397
- }),
398
- },
399
- )
400
- const text = await response.text()
401
- if (!response.ok) throw responseError(response.status, text)
402
- let parsed: unknown
403
- try {
404
- parsed = JSON.parse(text)
405
- } catch {
406
- throw new Error('RNX Cloud simulator create returned unreadable JSON')
407
- }
408
- if (!isRecord(parsed) || parsed.boxId !== this.session.boxId) {
409
- throw new Error('RNX Cloud simulator create returned an invalid box id')
410
- }
411
- const simulator = readSimulatorCreate(
412
- parsed.simulator,
413
- this.session.artifact.id,
414
- this.session.artifact.bytes,
415
- )
416
- const pooled = new NanoBox({
417
- ...this.session,
418
- simId: simulator.simId,
419
- simulatorToken: simulator.token,
420
- claim: simulator.claim,
421
- })
422
- await pooled.confirm()
423
- return pooled
424
- }
425
-
426
395
  async close(): Promise<void> {
427
396
  const response = await fetch(
428
397
  `${this.session.origin}/v1/boxes/${encodeURIComponent(this.session.boxId)}`,
@@ -438,7 +407,9 @@ export class NanoBox {
438
407
  }
439
408
  }
440
409
 
441
- export async function createNanoBox(input: CreateNanoBoxInput): Promise<NanoBox> {
410
+ export async function createRemoteSimulator(
411
+ input: CreateRemoteSimulatorInput,
412
+ ): Promise<RemoteSimulator> {
442
413
  if (input.artifact.byteLength > RNX_CLOUD_MAX_ARTIFACT_BYTES) {
443
414
  throw new Error(
444
415
  `rnx Cloud artifact is ${input.artifact.byteLength} bytes and exceeds the ${RNX_CLOUD_MAX_ARTIFACT_BYTES}-byte limit`,
@@ -484,9 +455,8 @@ export async function createNanoBox(input: CreateNanoBoxInput): Promise<NanoBox>
484
455
  throw new Error('RNX Cloud create returned unreadable JSON')
485
456
  }
486
457
  const created = readBoxCreate(parsed, input.artifact.byteLength, sha256)
487
- const box = new NanoBox({
458
+ const simulator = new RemoteSimulator({
488
459
  origin,
489
- authorization: input.authorization,
490
460
  boxId: created.receipt.boxId,
491
461
  boxToken: created.boxToken,
492
462
  simId: created.receipt.simulator.simId,
@@ -494,6 +464,6 @@ export async function createNanoBox(input: CreateNanoBoxInput): Promise<NanoBox>
494
464
  claim: created.receipt.simulator.claim,
495
465
  artifact: created.receipt.artifact,
496
466
  })
497
- await box.confirm()
498
- return box
467
+ await simulator.confirm()
468
+ return simulator
499
469
  }
@@ -1,4 +1,4 @@
1
- // rnx metro plugin for opt-in dev shell and production bundle annotation.
1
+ // rnx metro plugin for dev shell and bundle module identity annotation.
2
2
  // shell assets come from an engine runtime under ~/.rnx/runtimes/, fetched
3
3
  // by `rnx runtime install`. ordinary localhost requests
4
4
  // follow active; a version-owned localhost origin stays on its exact directory
@@ -6,19 +6,13 @@
6
6
  //
7
7
  // usage in metro.config.js:
8
8
  // const { withRNX } = require('rnxsim/metro')
9
- // module.exports = withRNX(getDefaultConfig(__dirname), { devServer: true })
9
+ // module.exports = withRNX(getDefaultConfig(__dirname))
10
10
 
11
11
  import fs from 'fs'
12
12
  import { createRequire } from 'module'
13
13
  import { randomUUID } from 'node:crypto'
14
14
  import path from 'path'
15
- import {
16
- openRnxDesktopWhenMetroIsReady,
17
- resolveRnxDesktopAppName,
18
- resolveRnxDesktopLaunchTarget,
19
- tagRnxBundleForPlugin,
20
- type RnxDesktopOpenOption,
21
- } from './dev-server-open'
15
+ import { tagRnxBundleForPlugin } from './dev-server-open'
22
16
  import {
23
17
  handleReplacementModuleRequest,
24
18
  isReplacementModuleRequestUrl,
@@ -71,26 +65,6 @@ type MetroSerializer = (
71
65
  options: MetroSerializerOptions,
72
66
  ) => MetroSerializerResult | Promise<MetroSerializerResult>
73
67
 
74
- export interface RNXMetroOptions {
75
- bundleUrl?: string
76
- devServer?: boolean
77
- open?: RnxDesktopOpenOption
78
- /**
79
- * append the `moduleId -> sourcePath` identity footer to every production
80
- * bundle. resolution is untouched, so the annotated artifact is the same
81
- * bundle the app ships and rnx can load it directly.
82
- */
83
- productionBundles?: boolean
84
- /**
85
- * append the `moduleId -> sourcePath` identity footer to every development
86
- * bundle. resolution and transforms stay ordinary, while Metro keeps verbose
87
- * module names and its source map.
88
- */
89
- developmentBundles?: boolean
90
- }
91
-
92
- // the slice of metro's config we touch — keeps `T` open for whatever
93
- // shape consumers pass while letting us extend `server.enhanceMiddleware`.
94
68
  type MetroConfigWithServer = {
95
69
  projectRoot?: string
96
70
  serializer?: {
@@ -160,7 +134,7 @@ function appendRNXModulePaths(
160
134
  const dependencies = graph.dependencies
161
135
  const createModuleId = options.createModuleId
162
136
  if (!dependencies || !createModuleId) {
163
- throw new Error('rnxsim: Metro did not provide the module graph and createModuleId')
137
+ return result
164
138
  }
165
139
 
166
140
  const modulePaths: Record<string, string> = {}
@@ -208,23 +182,9 @@ function appendRNXModulePaths(
208
182
  : { ...result, code: appendRNXMetroModuleIdentityFooter(result.code, identity) }
209
183
  }
210
184
 
211
- export function withRNX<T extends Record<string, unknown>>(
212
- config: T,
213
- options: RNXMetroOptions = {},
214
- ): T {
215
- if (
216
- !options.devServer &&
217
- !options.open &&
218
- !options.productionBundles &&
219
- !options.developmentBundles
220
- ) {
221
- return config
222
- }
223
-
185
+ export function withRNX<T extends Record<string, unknown>>(config: T): T {
224
186
  const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
225
- const appName = resolveRnxDesktopAppName(options.open, cfgProjectRoot)
226
- const configuredBundleUrl =
227
- options.bundleUrl || '/index.bundle?platform=ios&dev=true&hot=true&minify=false'
187
+ const configuredBundleUrl = '/index.bundle?platform=ios&dev=true&hot=true&minify=false'
228
188
 
229
189
  const cfgServer = (config as MetroConfigWithServer).server
230
190
  const port = resolveMetroPort(cfgServer)
@@ -234,42 +194,29 @@ export function withRNX<T extends Record<string, unknown>>(
234
194
  `http://localhost:${port}`,
235
195
  metroServerId,
236
196
  cfgProjectRoot,
237
- appName,
238
197
  )
239
198
  const cfgSerializer = (config as MetroConfigWithServer).serializer
240
199
  const existingEnhance = cfgServer?.enhanceMiddleware
241
200
  const existingSerializer = cfgSerializer?.customSerializer
242
- let productionConfig: Partial<MetroConfigWithServer> = {}
243
-
244
- if (options.productionBundles || options.developmentBundles) {
245
- let defaultSerializer: MetroSerializer | undefined
246
-
247
- productionConfig = {
248
- serializer: {
249
- ...cfgSerializer,
250
- customSerializer: async (
251
- entryPoint: string,
252
- preModules: readonly unknown[],
253
- graph: MetroSerializerGraph,
254
- serializerOptions: MetroSerializerOptions,
255
- ) => {
256
- const serializer =
257
- existingSerializer ||
258
- (defaultSerializer ||= createMetroDefaultSerializer(cfgProjectRoot))
259
- const result = await serializer(
260
- entryPoint,
261
- preModules,
262
- graph,
263
- serializerOptions,
264
- )
265
- const dev = serializerOptions.dev
266
- return (dev === false && options.productionBundles === true) ||
267
- (dev === true && options.developmentBundles === true)
268
- ? appendRNXModulePaths(result, graph, serializerOptions, cfgProjectRoot)
269
- : result
270
- },
201
+
202
+ let defaultSerializer: MetroSerializer | undefined
203
+
204
+ const serializerConfig = {
205
+ serializer: {
206
+ ...cfgSerializer,
207
+ customSerializer: async (
208
+ entryPoint: string,
209
+ preModules: readonly unknown[],
210
+ graph: MetroSerializerGraph,
211
+ serializerOptions: MetroSerializerOptions,
212
+ ) => {
213
+ const serializer =
214
+ existingSerializer ||
215
+ (defaultSerializer ||= createMetroDefaultSerializer(cfgProjectRoot))
216
+ const result = await serializer(entryPoint, preModules, graph, serializerOptions)
217
+ return appendRNXModulePaths(result, graph, serializerOptions, cfgProjectRoot)
271
218
  },
272
- }
219
+ },
273
220
  }
274
221
 
275
222
  const serverConfig = {
@@ -305,26 +252,6 @@ export function withRNX<T extends Record<string, unknown>>(
305
252
  const url = req.url || ''
306
253
  const pathname = url.split('?')[0]
307
254
 
308
- if (pathname === '/__server-scan') {
309
- const absoluteBundleUrl = bundleUrl.startsWith('http')
310
- ? bundleUrl
311
- : `http://localhost:${port}${bundleUrl}`
312
- res.setHeader('content-type', 'application/json')
313
- res.end(
314
- JSON.stringify([
315
- {
316
- port,
317
- framework: 'metro',
318
- rnxServerId: metroServerId,
319
- bundleUrl: absoluteBundleUrl,
320
- ...(appName ? { projectName: appName } : {}),
321
- lastSeen: Date.now(),
322
- },
323
- ]),
324
- )
325
- return
326
- }
327
-
328
255
  const runtimeRoot = resolveRuntimeRootForRequestHost(req.headers?.host)
329
256
 
330
257
  if (!runtimeRoot) {
@@ -371,10 +298,10 @@ export function withRNX<T extends Record<string, unknown>>(
371
298
  if (!ext) {
372
299
  const htmlPath = path.join(runtimeRoot, 'index.html')
373
300
  let html = fs.readFileSync(htmlPath, 'utf8')
374
- html = html.replace(
375
- '</head>',
376
- `<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleUrl)});history.replaceState(null,'','${prefix}/'+u.search)})()</script></head>`,
377
- )
301
+ const script = `<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleUrl)});history.replaceState(null,'','${prefix}/'+u.search)})()</script>`
302
+ html = html.includes('</head>')
303
+ ? html.replace('</head>', `${script}</head>`)
304
+ : `${html}${script}`
378
305
  res.setHeader('content-type', 'text/html')
379
306
  res.end(html)
380
307
  return
@@ -391,20 +318,10 @@ export function withRNX<T extends Record<string, unknown>>(
391
318
  },
392
319
  }
393
320
 
394
- if (appName && !process.env.CI && !process.env.RNX_NO_OPEN) {
395
- void openRnxDesktopWhenMetroIsReady({
396
- port,
397
- bundleUrl: resolveRnxDesktopLaunchTarget(port, bundleUrl),
398
- appName,
399
- projectRoot: cfgProjectRoot,
400
- serverId: metroServerId,
401
- })
402
- }
403
-
404
321
  return {
405
322
  ...config,
406
- ...productionConfig,
407
- ...(options.devServer || options.open ? { server: serverConfig } : {}),
323
+ ...serializerConfig,
324
+ server: serverConfig,
408
325
  } as T
409
326
  }
410
327
 
@@ -493,8 +493,17 @@ export type WorkerBootClosure = {
493
493
  }
494
494
 
495
495
  const ENGINE_WORKER_BOOT_CLOSURES: WorkerBootClosure[] = [
496
- { entry: 'src/render-worker/shell-worker-entry.ts', maxBytes: 3_480_000 },
497
- { entry: 'src/render-worker/compositor-worker-entry.ts', maxBytes: 1_255_000 },
496
+ // both caps went up by about half a megabyte for fontkit, which replaced
497
+ // opentype.js as the app-font parser so woff2 faces (what expo-font ships to
498
+ // iOS) get real vertical metrics instead of measuring at CanvasKit's rounded
499
+ // paragraph height. these workers inline their dynamic imports, so all of it
500
+ // lands in the boot file even though only an app that ships a font evaluates
501
+ // it. removing it again means the shell and compositor taking the parsed
502
+ // names and metrics off the wire from the tenant instead of re-parsing the
503
+ // same bytes in three contexts, behind a static import boundary so the parser
504
+ // is not reachable from these two entries at all.
505
+ { entry: 'src/render-worker/shell-worker-entry.ts', maxBytes: 4_020_000 },
506
+ { entry: 'src/render-worker/compositor-worker-entry.ts', maxBytes: 1_835_000 },
498
507
  ]
499
508
 
500
509
  export function workerBootChunkPlugin(closures: WorkerBootClosure[]): Plugin {