vxrn 1.26.0 → 1.26.1

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 (42) hide show
  1. package/dist/plugins/reactNativeDevServer.mjs +123 -42
  2. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  3. package/dist/plugins/reactNativeDevServer.native.js +209 -44
  4. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  5. package/dist/rn-commands/bundle/buildBundle.mjs +29 -10
  6. package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
  7. package/dist/rn-commands/bundle/buildBundle.native.js +29 -10
  8. package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
  9. package/dist/utils/createNativeDevEngine.mjs +220 -83
  10. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  11. package/dist/utils/createNativeDevEngine.native.js +257 -84
  12. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  13. package/dist/utils/createNativeDevEngine.test.mjs +285 -23
  14. package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
  15. package/dist/utils/createNativeDevEngine.test.native.js +319 -23
  16. package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
  17. package/dist/utils/patches.test.mjs.map +1 -1
  18. package/dist/utils/patches.test.native.js.map +1 -1
  19. package/dist/utils/symbolicateNativeStack.mjs +66 -0
  20. package/dist/utils/symbolicateNativeStack.mjs.map +1 -0
  21. package/dist/utils/symbolicateNativeStack.native.js +70 -0
  22. package/dist/utils/symbolicateNativeStack.native.js.map +1 -0
  23. package/dist/utils/symbolicateNativeStack.test.mjs +96 -0
  24. package/dist/utils/symbolicateNativeStack.test.mjs.map +1 -0
  25. package/dist/utils/symbolicateNativeStack.test.native.js +96 -0
  26. package/dist/utils/symbolicateNativeStack.test.native.js.map +1 -0
  27. package/package.json +12 -11
  28. package/src/plugins/reactNativeDevServer.ts +162 -50
  29. package/src/rn-commands/bundle/buildBundle.ts +28 -9
  30. package/src/utils/createNativeDevEngine.test.ts +581 -193
  31. package/src/utils/createNativeDevEngine.ts +434 -115
  32. package/src/utils/patches.test.ts +1 -4
  33. package/src/utils/symbolicateNativeStack.test.ts +78 -0
  34. package/src/utils/symbolicateNativeStack.ts +129 -0
  35. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  36. package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
  37. package/types/utils/createNativeDevEngine.d.ts +23 -4
  38. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  39. package/types/utils/symbolicateNativeStack.d.ts +46 -0
  40. package/types/utils/symbolicateNativeStack.d.ts.map +1 -0
  41. package/types/utils/symbolicateNativeStack.test.d.ts +2 -0
  42. package/types/utils/symbolicateNativeStack.test.d.ts.map +1 -0
@@ -11,6 +11,12 @@ import { readFile } from 'node:fs/promises'
11
11
  import { createDevMiddleware } from '@react-native/dev-middleware'
12
12
  import { createNativeDevEngine } from '../utils/createNativeDevEngine'
13
13
  import { getBoundPort } from '../utils/getBoundPort'
14
+ import {
15
+ getNativeFramePlatform,
16
+ isNativeBundleFrame,
17
+ symbolicateNativeStack,
18
+ type NativeStackFrame,
19
+ } from '../utils/symbolicateNativeStack'
14
20
 
15
21
  type ClientMessage = {
16
22
  type: 'client-log'
@@ -205,7 +211,7 @@ export function createReactNativeDevServerPlugin(
205
211
  }
206
212
  addConnectedNativeClient()
207
213
 
208
- socket.on('message', async (message) => {
214
+ socket.on('message', (message) => {
209
215
  const value = message.toString()
210
216
  if (value === 'ping') {
211
217
  socket.send('pong')
@@ -219,8 +225,9 @@ export function createReactNativeDevServerPlugin(
219
225
  return
220
226
  }
221
227
  if (update.type === 'hmr:invalidate') {
222
- currentEngine.engine.triggerFullBuild()
223
- await currentEngine.engine.ensureLatestBuildOutput()
228
+ // the runtime could not accept an update, so restart it. the bundle
229
+ // it fetches on the way back settles the engine's pending work
230
+ // first, so it lands on the current output.
224
231
  socket.send(JSON.stringify({ type: 'hmr:reload' }))
225
232
  }
226
233
  })
@@ -237,6 +244,66 @@ export function createReactNativeDevServerPlugin(
237
244
  })
238
245
  })
239
246
 
247
+ const reloadNativeClients = (platform: string) => {
248
+ const reload = JSON.stringify({ type: 'hmr:reload' })
249
+ hmrWSS.clients.forEach((client) => {
250
+ const nativeClient = client as NativeHmrSocket
251
+ if (nativeClient.readyState === 1 && nativeClient.vxrnPlatform === platform) {
252
+ nativeClient.send(reload)
253
+ }
254
+ })
255
+ }
256
+
257
+ // a created route file: rolldown's addWatchFile on the route directories
258
+ // is supposed to report it, and the engine rebuilds only then. github's
259
+ // macos runners do not always surface that, so vite's watcher is the
260
+ // backup, still scoped to route files inside handleAddedFile.
261
+ //
262
+ // a deletion it cannot survive. rolldown's dev engine panics its worker
263
+ // when it retires a module that left the disk ("index out of bounds" out
264
+ // of oxc_index), and from then on every bundle request answers 500 even
265
+ // once the file comes back. that engine cannot be repaired, so drop it
266
+ // and let the next bundle request build a fresh one.
267
+ let deletionTimer: ReturnType<typeof setTimeout> | undefined
268
+ let addTimer: ReturnType<typeof setTimeout> | undefined
269
+ const addedFiles = new Set<string>()
270
+ server.watcher.on('add', (file) => {
271
+ if (file.split('/').some((segment) => segment.startsWith('.'))) return
272
+ addedFiles.add(file)
273
+ clearTimeout(addTimer)
274
+ addTimer = setTimeout(() => {
275
+ const files = [...addedFiles]
276
+ addedFiles.clear()
277
+ for (const platform of Object.keys(devEngines)) {
278
+ const devEngine = devEngines[platform]
279
+ if (!devEngine) continue
280
+ for (const added of files) {
281
+ devEngine.handleAddedFile(added).catch((error) => {
282
+ console.error(`[vxrn] handling added ${added} for ${platform} failed`, error)
283
+ })
284
+ }
285
+ }
286
+ }, 50)
287
+ })
288
+ server.watcher.on('unlink', (file) => {
289
+ // vite's watcher already skips node_modules and .git. keep out the rest
290
+ // of what a dev server writes into a project while it runs: caches and
291
+ // build output under a dot directory, editor swap files.
292
+ if (file.split('/').some((segment) => segment.startsWith('.'))) return
293
+ // one pass for a burst (a branch switch, a generator, a rename)
294
+ clearTimeout(deletionTimer)
295
+ deletionTimer = setTimeout(() => {
296
+ for (const platform of Object.keys(devEngines)) {
297
+ const devEngine = devEngines[platform]
298
+ if (!devEngine) continue
299
+ devEngines[platform] = null
300
+ devEngineCreating[platform] = null
301
+ devEngine.close().catch(() => {})
302
+ reloadNativeClients(platform)
303
+ }
304
+ }, 500)
305
+ })
306
+
240
307
  clientWSS.on('connection', (socket) => {
241
308
  socket.on('message', (messageRaw) => {
242
309
  const message = JSON.parse(messageRaw.toString()) as any as ClientMessage
@@ -267,6 +334,49 @@ export function createReactNativeDevServerPlugin(
267
334
  })
268
335
  })
269
336
 
337
+ const getDevEngine = async (platform: 'ios' | 'android') => {
338
+ if (!devEngines[platform]) {
339
+ // prevent duplicate creation from concurrent requests
340
+ if (!devEngineCreating[platform]) {
341
+ devEngineCreating[platform] = (async () => {
342
+ try {
343
+ console.info(`[vxrn] creating rolldown DevEngine for ${platform}...`)
344
+ devEngines[platform] = await createNativeDevEngine({
345
+ root,
346
+ port: getBoundPort(server),
347
+ host: typeof host === 'string' ? host : 'localhost',
348
+ platform,
349
+ serverUrl: `http://${typeof host === 'string' && host !== '0.0.0.0' ? host : 'localhost'}:${getBoundPort(server)}`,
350
+ onHmrUpdate: (update) => {
351
+ const msg = JSON.stringify(update)
352
+ // an update with no clientId is for every client on the
353
+ // platform: an error, or a reload after a full rebuild
354
+ const target = 'clientId' in update ? update.clientId : undefined
355
+ hmrWSS.clients.forEach((client) => {
356
+ const nativeClient = client as NativeHmrSocket
357
+ if (
358
+ nativeClient.readyState === 1 &&
359
+ nativeClient.vxrnPlatform === platform &&
360
+ (!target || nativeClient.vxrnClientId === target)
361
+ ) {
362
+ client.send(msg)
363
+ }
364
+ })
365
+ },
366
+ })
367
+ console.info(`[vxrn] rolldown DevEngine ready for ${platform}`)
368
+ } catch (err) {
369
+ // clear so next request retries instead of permanently failing
370
+ devEngineCreating[platform] = null
371
+ throw err
372
+ }
373
+ })()
374
+ }
375
+ await devEngineCreating[platform]
376
+ }
377
+ return devEngines[platform]!
378
+ }
379
+
270
380
  // React Native bundle handler
271
381
  const handleRNBundle: Connect.NextHandleFunction = async (req, res) => {
272
382
  const url = new URL(req.url!, `http://${req.headers.host}`)
@@ -281,7 +391,10 @@ export function createReactNativeDevServerPlugin(
281
391
  // here would mean a second engine per platform. production native bundles
282
392
  // come from the build command instead. say so rather than quietly serving
283
393
  // dev bytes to something that asked for production ones.
284
- if (url.searchParams.get('dev') === 'false' && !warnedProdBundleRequest.has(platform)) {
394
+ if (
395
+ url.searchParams.get('dev') === 'false' &&
396
+ !warnedProdBundleRequest.has(platform)
397
+ ) {
285
398
  warnedProdBundleRequest.add(platform)
286
399
  console.warn(
287
400
  `[vxrn] ${platform} bundle requested with dev=false, but the dev server only builds dev bundles. Serving a dev bundle. Use the build command for a production one.`
@@ -289,50 +402,10 @@ export function createReactNativeDevServerPlugin(
289
402
  }
290
403
 
291
404
  try {
292
- const bundle = await (async () => {
293
- if (!devEngines[platform]) {
294
- // prevent duplicate creation from concurrent requests
295
- if (!devEngineCreating[platform]) {
296
- devEngineCreating[platform] = (async () => {
297
- try {
298
- console.info(`[vxrn] creating rolldown DevEngine for ${platform}...`)
299
- devEngines[platform] = await createNativeDevEngine({
300
- root,
301
- port: getBoundPort(server),
302
- host: typeof host === 'string' ? host : 'localhost',
303
- platform,
304
- serverUrl: `http://${typeof host === 'string' && host !== '0.0.0.0' ? host : 'localhost'}:${getBoundPort(server)}`,
305
- onHmrUpdate: (update) => {
306
- const msg = JSON.stringify(update)
307
- hmrWSS.clients.forEach((client) => {
308
- const nativeClient = client as NativeHmrSocket
309
- if (
310
- nativeClient.readyState === 1 &&
311
- nativeClient.vxrnPlatform === platform &&
312
- (update.type === 'hmr:error' ||
313
- nativeClient.vxrnClientId === update.clientId)
314
- ) {
315
- client.send(msg)
316
- }
317
- })
318
- },
319
- })
320
- console.info(`[vxrn] rolldown DevEngine ready for ${platform}`)
321
- } catch (err) {
322
- // clear so next request retries instead of permanently failing
323
- devEngineCreating[platform] = null
324
- throw err
325
- }
326
- })()
327
- }
328
- await devEngineCreating[platform]
329
- }
330
-
331
- return await devEngines[platform]!.getBundle().then((r) => r.code)
332
- })()
405
+ const bundle = await (await getDevEngine(platform)).getBundle()
333
406
 
334
407
  res.writeHead(200, { 'Content-Type': 'text/javascript' })
335
- res.end(bundle)
408
+ res.end(bundle.code)
336
409
  } catch (err) {
337
410
  console.error(` Error building React Native bundle`)
338
411
  console.error(err)
@@ -353,10 +426,49 @@ export function createReactNativeDevServerPlugin(
353
426
  }
354
427
  })
355
428
 
356
- // Symbolicate endpoint
357
- server.middlewares.use('/symbolicate', (_req, res) => {
358
- res.writeHead(200, { 'Content-Type': 'text/plain' })
359
- res.end('TODO')
429
+ // resolve a device stack back to authored files through the dev bundle's
430
+ // source map. react native posts here from LogBox and from console traces.
431
+ server.middlewares.use('/symbolicate', async (req, res) => {
432
+ try {
433
+ const chunks: Buffer[] = []
434
+ for await (const chunk of req) chunks.push(chunk as Buffer)
435
+ const body: { stack?: NativeStackFrame[] } = JSON.parse(
436
+ Buffer.concat(chunks).toString('utf8')
437
+ )
438
+ const stack = body.stack
439
+ if (!Array.isArray(stack)) {
440
+ throw new Error('expected a `stack` array in the request body')
441
+ }
442
+
443
+ const bundleFrame = stack.find((frame) => isNativeBundleFrame(frame.file))
444
+ if (!bundleFrame) {
445
+ // nothing in this stack came from the bundle, so there is nothing to
446
+ // resolve. hand it straight back rather than inventing frames.
447
+ res.writeHead(200, { 'Content-Type': 'application/json' })
448
+ res.end(JSON.stringify({ stack, codeFrame: null }))
449
+ return
450
+ }
451
+
452
+ const framePlatform = getNativeFramePlatform(bundleFrame.file)
453
+ const platform = validPlatforms[framePlatform ?? '']
454
+ if (!platform) {
455
+ throw new Error(
456
+ `could not tell which platform "${bundleFrame.file}" was bundled for`
457
+ )
458
+ }
459
+
460
+ const bundle = await (await getDevEngine(platform)).getBundle()
461
+ res.writeHead(200, { 'Content-Type': 'application/json' })
462
+ res.end(JSON.stringify(symbolicateNativeStack(stack, bundle.map)))
463
+ } catch (err) {
464
+ console.error(`[vxrn] symbolicate failed`, err)
465
+ res.writeHead(500, { 'Content-Type': 'application/json' })
466
+ res.end(
467
+ JSON.stringify({
468
+ error: err instanceof Error ? err.message : String(err),
469
+ })
470
+ )
471
+ }
360
472
  })
361
473
  },
362
474
  }
@@ -1,5 +1,5 @@
1
- import path from 'node:path'
2
1
  import FSExtra from 'fs-extra'
2
+ import { clearTransformCache } from '@vxrn/compiler'
3
3
  import {
4
4
  bundle as metroBundle,
5
5
  type BundleCommandArgs,
@@ -18,15 +18,29 @@ export async function buildBundle(
18
18
  const {
19
19
  platform, // Android is not supported yet.
20
20
  dev,
21
- // bundleEncoding, // Not supported, we are using utf8.
22
- // sourcemapUseAbsolutePath,
23
- // unstableTransformProfile,
24
- // resetCache, // Currently we are not using cache for production builds.
25
- // readGlobalCache,
26
- // entryFile, // Not supported. With VxRN, we are using a static entry file.
27
21
  bundleOutput,
22
+ // React Native's `--bundle-encoding` names a Node Buffer encoding.
23
+ bundleEncoding = 'utf8',
28
24
  assetsDest,
29
- // minify, // Minification is not supported.
25
+ // `--minify` overrides React Native's default of "minify unless this is a
26
+ // dev bundle". undefined means the flag was not passed.
27
+ minify,
28
+ // `--reset-cache` drops the compiler's transform cache, the only cache a
29
+ // rolldown production build reads.
30
+ resetCache,
31
+ // `--entry-file` names the React Native root module. One generates its own
32
+ // native entry from the route tree instead, and __vxrnNativeEntryFile is
33
+ // the supported way to point the build somewhere else.
34
+ // `--read-global-cache` / `--max-workers` / `--config` / `--transformer` /
35
+ // `--resolver-option` configure Metro's global cache, worker pool and
36
+ // resolver. A rolldown production build has none of them: it transforms
37
+ // from source in-process. React Native's own bundle command ignores
38
+ // `--read-global-cache` too.
39
+ // `--unstable-transform-profile` picks a JS engine to target. This pipeline
40
+ // always downlevels for Hermes, which is what every profile it accepts
41
+ // ('default', 'hermes', 'hermes-canary') can run.
42
+ // `--sourcemap-use-absolute-path`, `--sourcemap-sources-root`,
43
+ // `--asset-catalog-dest` and `--indexed-ram-bundle` are not implemented.
30
44
  } = args
31
45
 
32
46
  const { root } = ctx
@@ -53,6 +67,10 @@ export async function buildBundle(
53
67
  process.env.NODE_ENV = 'production'
54
68
  }
55
69
 
70
+ if (resetCache) {
71
+ clearTransformCache()
72
+ }
73
+
56
74
  console.info(`[vxrn] building native bundle for ${platform}...`)
57
75
  const nativeEntryFile = (globalThis as { __vxrnNativeEntryFile?: unknown })
58
76
  .__vxrnNativeEntryFile
@@ -67,6 +85,7 @@ export async function buildBundle(
67
85
  serverUrl: process.env.ONE_SERVER_URL,
68
86
  assetsDest,
69
87
  sourcemap: !!args.sourcemapOutput,
88
+ minify,
70
89
  })
71
90
  const builtBundle = result.code
72
91
 
@@ -76,7 +95,7 @@ export async function buildBundle(
76
95
  }
77
96
 
78
97
  console.info(`Writing bundle to ${bundleOutput}...`)
79
- FSExtra.writeFileSync(bundleOutput, builtBundle, { encoding: 'utf8' })
98
+ FSExtra.writeFileSync(bundleOutput, builtBundle, { encoding: bundleEncoding })
80
99
  console.info('Done.')
81
100
 
82
101
  // Prevent the process not getting exited for some unknown reason.