sootsim 0.1.298 → 0.1.299

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 (149) hide show
  1. package/dist-cli/bin.js +5 -5
  2. package/dist-cli/chunks/{agent-U3HXOT6B.js → agent-3UVOB3T3.js} +2 -2
  3. package/dist-cli/chunks/{agent-wrapper-LJOPJUAN.js → agent-wrapper-XVOWC72E.js} +2 -2
  4. package/dist-cli/chunks/{app-fonts-4EVLV2BX.js → app-fonts-C222PFAQ.js} +2 -2
  5. package/dist-cli/chunks/{assert-NNKBTEO7.js → assert-2MHAZDEL.js} +2 -2
  6. package/dist-cli/chunks/{auth-SF2WR2K2.js → auth-7DTOEHC2.js} +2 -2
  7. package/dist-cli/chunks/{beta-SCLWNOHI.js → beta-WOHZHXPE.js} +2 -2
  8. package/dist-cli/chunks/{camera-TZJ2Z3TZ.js → camera-XCUE5ZNM.js} +2 -2
  9. package/dist-cli/chunks/{chunk-MHCSRMK4.js → chunk-23JBOG4P.js} +1 -1
  10. package/dist-cli/chunks/{chunk-IBMZ7A6M.js → chunk-2FTQEDJC.js} +2 -2
  11. package/dist-cli/chunks/{chunk-IRMKZSWK.js → chunk-37ACY2R3.js} +2 -2
  12. package/dist-cli/chunks/{chunk-APNG6X2R.js → chunk-444BQYY3.js} +2 -2
  13. package/dist-cli/chunks/{chunk-BUM4RR4G.js → chunk-4TYUHNHI.js} +2 -2
  14. package/dist-cli/chunks/{chunk-G3NMMKBL.js → chunk-5GRQIA5R.js} +2 -2
  15. package/dist-cli/chunks/{chunk-MFMFNCBK.js → chunk-5VX2Y3ZB.js} +2 -2
  16. package/dist-cli/chunks/{chunk-WV6CBQYZ.js → chunk-6RH6PEO3.js} +1 -1
  17. package/dist-cli/chunks/{chunk-XITV33JC.js → chunk-ALOQJP4C.js} +2 -2
  18. package/dist-cli/chunks/{chunk-ZCHO67PX.js → chunk-B6TCNXJO.js} +2 -2
  19. package/dist-cli/chunks/{chunk-JM36NHNU.js → chunk-CBRE6JLP.js} +1 -1
  20. package/dist-cli/chunks/{chunk-EYBWIEDO.js → chunk-DGWM3X4S.js} +2 -2
  21. package/dist-cli/chunks/{chunk-KVYZYYSY.js → chunk-DUGY7KHN.js} +1 -1
  22. package/dist-cli/chunks/{chunk-TTFO75GL.js → chunk-E2XMG2HC.js} +1 -1
  23. package/dist-cli/chunks/{chunk-ZBETYTSD.js → chunk-EJC4A2ZH.js} +1 -1
  24. package/dist-cli/chunks/{chunk-HAJSNFUY.js → chunk-F57XAX5N.js} +1 -1
  25. package/dist-cli/chunks/{chunk-NRC73NVF.js → chunk-FLTBPGAQ.js} +2 -2
  26. package/dist-cli/chunks/{chunk-4H3Y3TGT.js → chunk-FMQ772D7.js} +1 -1
  27. package/dist-cli/chunks/{chunk-AHMMVYVE.js → chunk-G377NNEV.js} +1 -1
  28. package/dist-cli/chunks/{chunk-QZCVY5BX.js → chunk-HM42NCGX.js} +1 -1
  29. package/dist-cli/chunks/{chunk-ANFOERLP.js → chunk-HVQA443X.js} +1 -1
  30. package/dist-cli/chunks/{chunk-PLEZAZTJ.js → chunk-I4NK5VBB.js} +2 -2
  31. package/dist-cli/chunks/chunk-IGKB2HLF.js +4 -0
  32. package/dist-cli/chunks/{chunk-HGSYUEQP.js → chunk-ITSHLH5U.js} +2 -2
  33. package/dist-cli/chunks/{chunk-AT3NTZPP.js → chunk-JXHCBXUD.js} +3 -3
  34. package/dist-cli/chunks/{chunk-HOQD6TZN.js → chunk-KJQJ5VPA.js} +1 -1
  35. package/dist-cli/chunks/{chunk-CK3MTK2D.js → chunk-L3FA7WE5.js} +2 -2
  36. package/dist-cli/chunks/{chunk-SQR7NH32.js → chunk-L7GNKY3A.js} +1 -1
  37. package/dist-cli/chunks/{chunk-U5TY536V.js → chunk-LEHK72DK.js} +2 -2
  38. package/dist-cli/chunks/{chunk-GR7USTCW.js → chunk-LFWATDB3.js} +2 -2
  39. package/dist-cli/chunks/{chunk-TB77ANGH.js → chunk-MKIABOQK.js} +2 -2
  40. package/dist-cli/chunks/{chunk-UTUBMEFS.js → chunk-MSYFSZKB.js} +1 -1
  41. package/dist-cli/chunks/{chunk-6YIZZDG2.js → chunk-MTOADQY2.js} +2 -2
  42. package/dist-cli/chunks/{chunk-WDKLXBQR.js → chunk-ONQYJYUM.js} +1 -1
  43. package/dist-cli/chunks/{chunk-EH4B5ZQ4.js → chunk-PHBVCGV3.js} +2 -2
  44. package/dist-cli/chunks/{chunk-HG63AS7K.js → chunk-Q34734UC.js} +1 -1
  45. package/dist-cli/chunks/{chunk-MTL4DPUK.js → chunk-QMDKE3C6.js} +1 -1
  46. package/dist-cli/chunks/{chunk-FTMTIUBK.js → chunk-QPPG5YZS.js} +2 -2
  47. package/dist-cli/chunks/{chunk-7ZOI5KJZ.js → chunk-RWXJ6OS4.js} +2 -2
  48. package/dist-cli/chunks/{chunk-VFI23QUE.js → chunk-UNFMIX7B.js} +1 -1
  49. package/dist-cli/chunks/{chunk-X3STT7I3.js → chunk-VR4ZUUA4.js} +1 -1
  50. package/dist-cli/chunks/{chunk-GXVG5LJG.js → chunk-W3H6FBWV.js} +2 -2
  51. package/dist-cli/chunks/{chunk-J42Y66IM.js → chunk-WCAT3UC4.js} +2 -2
  52. package/dist-cli/chunks/{chunk-CXSMZLI6.js → chunk-WM4RNTPN.js} +3 -3
  53. package/dist-cli/chunks/{chunk-3R4W2SQB.js → chunk-WMLVFFXE.js} +3 -3
  54. package/dist-cli/chunks/{chunk-GM2TRB5Z.js → chunk-XMOYKSSM.js} +2 -2
  55. package/dist-cli/chunks/{chunk-PYUI2PHI.js → chunk-YEE3MKPH.js} +2 -2
  56. package/dist-cli/chunks/{chunk-CAEQ7AOI.js → chunk-ZEXLS32P.js} +3 -3
  57. package/dist-cli/chunks/{chunk-5VGTEGDT.js → chunk-ZRDU2ENE.js} +2 -2
  58. package/dist-cli/chunks/{cleanup-YZPCNPWE.js → cleanup-O6SD44HN.js} +2 -2
  59. package/dist-cli/chunks/{cli-version-ECA6G6JQ.js → cli-version-PIYRRI52.js} +2 -2
  60. package/dist-cli/chunks/{compat-OM7ODBMO.js → compat-AWJLEWTH.js} +2 -2
  61. package/dist-cli/chunks/{config-ZIX6LPFG.js → config-6PLYOSBK.js} +2 -2
  62. package/dist-cli/chunks/{control-BY6UAMKN.js → control-K4QMNPXN.js} +2 -2
  63. package/dist-cli/chunks/daemon-HG6LHW3O.js +4 -0
  64. package/dist-cli/chunks/{debug-MVPFRIQA.js → debug-YCJCLH35.js} +3 -3
  65. package/dist-cli/chunks/{desktop-ZXH7NRHW.js → desktop-NRUWYSTV.js} +3 -3
  66. package/dist-cli/chunks/{detox-4RZ5UER3.js → detox-XTERPTZN.js} +2 -2
  67. package/dist-cli/chunks/{device-7R4BSFMS.js → device-G4KJITB6.js} +2 -2
  68. package/dist-cli/chunks/{diagnose-4JAFIXWD.js → diagnose-CKZOV5PL.js} +2 -2
  69. package/dist-cli/chunks/disk-cleanup-LX6FRBNM.js +4 -0
  70. package/dist-cli/chunks/drivers-X6XG4AX4.js +4 -0
  71. package/dist-cli/chunks/{film-Q7KZIGBH.js → film-4T3QY3VT.js} +3 -3
  72. package/dist-cli/chunks/flow-EQPPW77J.js +4 -0
  73. package/dist-cli/chunks/help-IRKWFF27.js +4 -0
  74. package/dist-cli/chunks/{hidden-runtime-alias-4ZUCLCVJ.js → hidden-runtime-alias-S2PPAYI3.js} +2 -2
  75. package/dist-cli/chunks/{home-paths-PVZPREN7.js → home-paths-MUZXK2LA.js} +2 -2
  76. package/dist-cli/chunks/inspect-VGNST6ZA.js +4 -0
  77. package/dist-cli/chunks/install-desktop-5H7A5R3Z.js +4 -0
  78. package/dist-cli/chunks/{login-UMP65DJG.js → login-RH2KRRBQ.js} +4 -4
  79. package/dist-cli/chunks/{logout-F45WBVJV.js → logout-GIEQBBFN.js} +2 -2
  80. package/dist-cli/chunks/{maestro-33RGEXP4.js → maestro-JEAOFJLT.js} +3 -3
  81. package/dist-cli/chunks/{maestro-generate-5SLZMOMB.js → maestro-generate-GAYZAFOV.js} +3 -3
  82. package/dist-cli/chunks/{mode-LZORZYWP.js → mode-V2OWWT7B.js} +2 -2
  83. package/dist-cli/chunks/{optional-demo-registry-DIWO7DG5.js → optional-demo-registry-CZSJJFLR.js} +2 -2
  84. package/dist-cli/chunks/{perf-VW3UTYCK.js → perf-SNBPEVZL.js} +2 -2
  85. package/dist-cli/chunks/{permissions-7JIR6QPO.js → permissions-MKMHM34X.js} +2 -2
  86. package/dist-cli/chunks/{record-A4MRKJ2E.js → record-55URP7AI.js} +3 -3
  87. package/dist-cli/chunks/{report-issue-JUWTB66Z.js → report-issue-I4JZBUDD.js} +2 -2
  88. package/dist-cli/chunks/runtime-XHMSVSWG.js +4 -0
  89. package/dist-cli/chunks/{screenshot-command-6LROQJPJ.js → screenshot-command-YGBR4Y2V.js} +3 -3
  90. package/dist-cli/chunks/{screenshots-capture-FMYE24EW.js → screenshots-capture-S5CP6OWZ.js} +2 -2
  91. package/dist-cli/chunks/{serve-64SRDCU5.js → serve-45NQNA7C.js} +2 -2
  92. package/dist-cli/chunks/{setup-2CUXSDQH.js → setup-HKTRNWH6.js} +2 -2
  93. package/dist-cli/chunks/{skills-IILJVUMS.js → skills-HUDGHVRT.js} +2 -2
  94. package/dist-cli/chunks/{storage-BUQQJAMD.js → storage-HOYYA2PS.js} +2 -2
  95. package/dist-cli/chunks/store-7QB3V42F.js +4 -0
  96. package/dist-cli/chunks/telemetry-HYW35ACO.js +4 -0
  97. package/dist-cli/chunks/{timeline-62RWUHSF.js → timeline-2S3FS3AA.js} +2 -2
  98. package/dist-cli/chunks/{upgrade-GHXDTO2A.js → upgrade-KZSYVT3E.js} +2 -2
  99. package/dist-cli/chunks/upload-P3CQI4RQ.js +4 -0
  100. package/dist-cli/chunks/{version-F4CKBMB6.js → version-BWNEYFPL.js} +2 -2
  101. package/dist-cli/chunks/{web-YM6GIFOU.js → web-DXSNL2J6.js} +2 -2
  102. package/dist-cli/chunks/{what-happened-UKBSKIGA.js → what-happened-VT4FO7XN.js} +2 -2
  103. package/dist-lib/agent-daemon-client.cjs +1 -1
  104. package/dist-lib/agent-events.cjs +1 -1
  105. package/dist-lib/agent-identity.cjs +1 -1
  106. package/dist-lib/agent-sessions.cjs +1 -1
  107. package/dist-lib/attached-projects.cjs +1 -1
  108. package/dist-lib/auth/shared-session.cjs +1 -1
  109. package/dist-lib/backend-origin.cjs +1 -1
  110. package/dist-lib/beta.cjs +1 -1
  111. package/dist-lib/beta.mjs +1 -1
  112. package/dist-lib/bridge-constants.cjs +1 -1
  113. package/dist-lib/cli-constants.cjs +1 -1
  114. package/dist-lib/config.cjs +1 -1
  115. package/dist-lib/detox/index.cjs +1 -1
  116. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  117. package/dist-lib/home-paths.cjs +1 -1
  118. package/dist-lib/host/bridge-host.cjs +1 -1
  119. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  120. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  121. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  122. package/dist-lib/host/websocket-proxy.cjs +1 -1
  123. package/dist-lib/index.cjs +1 -1
  124. package/dist-lib/jump-to-source-babel.cjs +1 -1
  125. package/dist-lib/menu.cjs +1 -1
  126. package/dist-lib/menu.mjs +1 -1
  127. package/dist-lib/metro.cjs +1 -1
  128. package/dist-lib/profiles.cjs +1 -1
  129. package/dist-lib/render-mode.cjs +1 -1
  130. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  131. package/dist-lib/sdk.cjs +1 -1
  132. package/dist-lib/sdk.mjs +1 -1
  133. package/dist-lib/skills.cjs +46 -44
  134. package/dist-lib/vite.cjs +1 -1
  135. package/package.json +1 -1
  136. package/src/vite-plugin.ts +452 -396
  137. package/src/worklets-babel.ts +4 -2
  138. package/dist-cli/chunks/chunk-THIBWEML.js +0 -4
  139. package/dist-cli/chunks/daemon-BQ2T53ZA.js +0 -4
  140. package/dist-cli/chunks/disk-cleanup-3E7GYLGW.js +0 -4
  141. package/dist-cli/chunks/drivers-2OW33GOL.js +0 -4
  142. package/dist-cli/chunks/flow-QTOSAQ5X.js +0 -4
  143. package/dist-cli/chunks/help-UE32FDEY.js +0 -4
  144. package/dist-cli/chunks/inspect-QWR2U3DK.js +0 -4
  145. package/dist-cli/chunks/install-desktop-R2ZM5ZJH.js +0 -4
  146. package/dist-cli/chunks/runtime-7LSOODXL.js +0 -4
  147. package/dist-cli/chunks/store-3Z2GSFGY.js +0 -4
  148. package/dist-cli/chunks/telemetry-2UMKQXU7.js +0 -4
  149. package/dist-cli/chunks/upload-H6RTYSLO.js +0 -4
@@ -23,9 +23,14 @@ import {
23
23
  reactNativeDeepStubsForBuildResolver,
24
24
  } from '../../compat/src/stub-manifest.ts'
25
25
  import { resolveSootsimBridgePort } from './bridge-constants.ts'
26
- import { shouldApplyWorkletsPlugin, transformWorkletsCode } from './worklets-babel.ts'
26
+ import {
27
+ REANIMATED_WORKLETIZATION_REGEX,
28
+ shouldApplyWorkletsPlugin,
29
+ transformWorkletsCode,
30
+ } from './worklets-babel.ts'
27
31
 
28
32
  const sootsimPluginRequire = createRequire(import.meta.url)
33
+ const virtualIdPrefix = String.fromCharCode(0)
29
34
 
30
35
  export interface SootSimOptions {
31
36
  // directory of an external RN app to load
@@ -347,8 +352,15 @@ export function sootsim(options: SootSimOptions = {}): Plugin[] {
347
352
  {
348
353
  name: 'sootsim-published-browser-internals',
349
354
  enforce: 'pre',
350
- resolveId(source, importer) {
351
- return resolvePublishedBrowserInternal(source, importer, publishedBrowserPackages)
355
+ resolveId: {
356
+ filter: { id: /^\./ },
357
+ handler(source, importer) {
358
+ return resolvePublishedBrowserInternal(
359
+ source,
360
+ importer,
361
+ publishedBrowserPackages,
362
+ )
363
+ },
352
364
  },
353
365
  },
354
366
  sootsimConfigPlugin(appDir, extraSources, publishedBrowserPackages),
@@ -366,8 +378,10 @@ export function sootsim(options: SootSimOptions = {}): Plugin[] {
366
378
  workletsBabelTransform(isAppSource),
367
379
  metroNativeResolve(isAppSource),
368
380
  reactNativeRequirePlugin(isAppSource),
369
- externalAppResolvePlugin(appDir, isAppSource, ownedPackages),
370
- externalAppTransformPlugin(isAppSource),
381
+ ...(appDir ? [externalAppResolvePlugin(appDir, isAppSource, ownedPackages)] : []),
382
+ ...(appDir || extraSources.length > 0
383
+ ? [externalAppTransformPlugin(isAppSource)]
384
+ : []),
371
385
  stubMissingNativeDeps(appDir),
372
386
  stubMissingImages(isAppSource),
373
387
  ]
@@ -386,21 +400,26 @@ function rnghUpstreamInternalResolvePlugin(): Plugin {
386
400
  return {
387
401
  name: 'sootsim-rngh-upstream-internal-resolve',
388
402
  enforce: 'pre',
389
- resolveId(source, importer) {
390
- if (!importer || !rnghLibModuleRoot) return null
391
- const importerPath = stripViteQuery(importer)
392
- if (!isPathInside(importerPath, rnghLibModuleRoot)) return null
393
- const sourcePath = source.replace(/\.(js|jsx|mjs|ts|tsx)$/, '')
394
- if (
395
- sourcePath === '../..' ||
396
- sourcePath.endsWith('/handlers/gestures/GestureDetector')
397
- ) {
398
- return rnghRootStubPath
399
- }
400
- if (sourcePath.endsWith('GestureHandlerRootViewContext')) {
401
- return rnghGestureHandlerRootViewContextShimPath
402
- }
403
- return null
403
+ resolveId: {
404
+ filter: {
405
+ id: /(?:^\.\.\/\.\.(?:\.(?:js|jsx|mjs|ts|tsx))?$|GestureDetector(?:\.(?:js|jsx|mjs|ts|tsx))?$|GestureHandlerRootViewContext(?:\.(?:js|jsx|mjs|ts|tsx))?$)/,
406
+ },
407
+ handler(source, importer) {
408
+ if (!importer || !rnghLibModuleRoot) return null
409
+ const importerPath = stripViteQuery(importer)
410
+ if (!isPathInside(importerPath, rnghLibModuleRoot)) return null
411
+ const sourcePath = source.replace(/\.(js|jsx|mjs|ts|tsx)$/, '')
412
+ if (
413
+ sourcePath === '../..' ||
414
+ sourcePath.endsWith('/handlers/gestures/GestureDetector')
415
+ ) {
416
+ return rnghRootStubPath
417
+ }
418
+ if (sourcePath.endsWith('GestureHandlerRootViewContext')) {
419
+ return rnghGestureHandlerRootViewContextShimPath
420
+ }
421
+ return null
422
+ },
404
423
  },
405
424
  }
406
425
  }
@@ -482,28 +501,35 @@ export function keyboardControllerBindingsRedirect(): Plugin {
482
501
  return {
483
502
  name: 'sootsim-keyboard-controller-bindings-redirect',
484
503
  enforce: 'pre',
485
- resolveId(source, importer) {
486
- if (!importer) return null
487
- if (!importer.includes('/react-native-keyboard-controller/')) return null
488
- // keep bindings imports — strip optional .native and any leading
489
- // `./`, `../`, `../../` so the depth doesn't matter.
490
- const basename = source.split('/').pop() ?? ''
491
- if (nativePlatformBasenames.has(basename)) {
492
- return (
493
- resolveNativePlatformFile(source, importer) ??
494
- (basename === 'findNodeHandle' ? findNodeHandleTarget : null)
495
- )
496
- }
497
- if (basename !== 'bindings' && basename !== 'bindings.native') return null
498
- // don't redirect bindings imports that already resolved to our file
499
- if (importer === target) return null
500
- return target
504
+ resolveId: {
505
+ filter: {
506
+ id: /(?:^|\/)(?:bindings(?:\.native)?|findNodeHandle|reanimated)$/,
507
+ },
508
+ handler(source, importer) {
509
+ if (!importer) return null
510
+ if (!importer.includes('/react-native-keyboard-controller/')) return null
511
+ // keep bindings imports — strip optional .native and any leading
512
+ // `./`, `../`, `../../` so the depth doesn't matter.
513
+ const basename = source.split('/').pop() ?? ''
514
+ if (nativePlatformBasenames.has(basename)) {
515
+ return (
516
+ resolveNativePlatformFile(source, importer) ??
517
+ (basename === 'findNodeHandle' ? findNodeHandleTarget : null)
518
+ )
519
+ }
520
+ if (basename !== 'bindings' && basename !== 'bindings.native') return null
521
+ // don't redirect bindings imports that already resolved to our file
522
+ if (importer === target) return null
523
+ return target
524
+ },
501
525
  },
502
- load(id) {
503
- if (id === findNodeHandleTarget) {
526
+ load: {
527
+ filter: {
528
+ id: new RegExp(`^${virtualIdPrefix}sootsim:rnkc-find-node-handle-native$`),
529
+ },
530
+ handler() {
504
531
  return `export { findNodeHandle } from ${JSON.stringify(rnShimPath)};`
505
- }
506
- return null
532
+ },
507
533
  },
508
534
  }
509
535
  }
@@ -549,58 +575,68 @@ export function reanimatedNativeSeamRedirect(): Plugin {
549
575
  return {
550
576
  name: 'sootsim-reanimated-native-seam-redirect',
551
577
  enforce: 'pre',
552
- resolveId(source, importer) {
553
- // upstream's `assertWorkletsVersion` (DEV-only) imports a build-time
554
- // script that does `require('react-native-worklets/package.json')`.
555
- // our flat-file stub for react-native-worklets has no such subpath,
556
- // so this throws under vite's worker build. resolve the package.json
557
- // import to a synthetic virtual id and load synthetic JSON below.
558
- if (source === 'react-native-worklets/package.json') {
559
- return '\0sootsim:react-native-worklets-package-json'
560
- }
561
- if (!importer) return null
562
- // no-op the assertWorkletsVersion validator — same reason. matches both
563
- // the bare import and the .js-suffixed resolved form some bundlers see.
564
- if (
565
- source === 'react-native-reanimated/scripts/validate-worklets-version' ||
566
- source === 'react-native-reanimated/scripts/validate-worklets-version.js'
567
- ) {
568
- return reanimatedValidateWorkletsVersionPath
569
- }
570
- if (!importer.includes('/react-native-reanimated/')) return null
571
- if (importer === target) return null
572
- // strip optional `.web` / `.native` extension and leading `./`s — the
573
- // redirect should match regardless of relative depth or platform suffix.
574
- const basename = (source.split('/').pop() ?? '').replace(
575
- /\.(native|web|ios|android)$/,
576
- '',
577
- )
578
- // keep fabricUtils on the SootSim Fabric public-instance contract. the
579
- // redirected module reads the constructor-attached handle for native
580
- // refs and uses ReactFabric's renderer lookup for composite refs.
581
- if (basename === 'fabricUtils' && importer !== reanimatedFabricUtilsRedirectPath) {
582
- return reanimatedFabricUtilsRedirectPath
583
- }
584
- if (!reanimatedNativeSeamFiles.has(basename)) return null
585
- if (process.env.SOOTSIM_REANIMATED_REDIRECT_DEBUG) {
586
- console.log(
587
- '[reanimatedRedirect]',
588
- basename,
589
- '<-',
590
- source,
591
- 'from',
592
- importer.slice(-80),
578
+ resolveId: {
579
+ filter: {
580
+ id: /(?:^react-native-worklets\/package\.json$|^react-native-reanimated\/scripts\/validate-worklets-version(?:\.js)?$|(?:^|\/)(?:fabricUtils|NativeReanimatedModule|scrollTo|measure|setNativeProps|dispatchCommand|setGestureState)(?:\.(?:native|web|ios|android))?$)/,
581
+ },
582
+ handler(source, importer) {
583
+ // upstream's `assertWorkletsVersion` (DEV-only) imports a build-time
584
+ // script that does `require('react-native-worklets/package.json')`.
585
+ // our flat-file stub for react-native-worklets has no such subpath,
586
+ // so this throws under vite's worker build. resolve the package.json
587
+ // import to a synthetic virtual id and load synthetic JSON below.
588
+ if (source === 'react-native-worklets/package.json') {
589
+ return '\0sootsim:react-native-worklets-package-json'
590
+ }
591
+ if (!importer) return null
592
+ // no-op the assertWorkletsVersion validator — same reason. matches both
593
+ // the bare import and the .js-suffixed resolved form some bundlers see.
594
+ if (
595
+ source === 'react-native-reanimated/scripts/validate-worklets-version' ||
596
+ source === 'react-native-reanimated/scripts/validate-worklets-version.js'
597
+ ) {
598
+ return reanimatedValidateWorkletsVersionPath
599
+ }
600
+ if (!importer.includes('/react-native-reanimated/')) return null
601
+ if (importer === target) return null
602
+ // strip optional `.web` / `.native` extension and leading `./`s — the
603
+ // redirect should match regardless of relative depth or platform suffix.
604
+ const basename = (source.split('/').pop() ?? '').replace(
605
+ /\.(native|web|ios|android)$/,
606
+ '',
593
607
  )
594
- }
595
- return target
608
+ // keep fabricUtils on the SootSim Fabric public-instance contract. the
609
+ // redirected module reads the constructor-attached handle for native
610
+ // refs and uses ReactFabric's renderer lookup for composite refs.
611
+ if (
612
+ basename === 'fabricUtils' &&
613
+ importer !== reanimatedFabricUtilsRedirectPath
614
+ ) {
615
+ return reanimatedFabricUtilsRedirectPath
616
+ }
617
+ if (!reanimatedNativeSeamFiles.has(basename)) return null
618
+ if (process.env.SOOTSIM_REANIMATED_REDIRECT_DEBUG) {
619
+ console.log(
620
+ '[reanimatedRedirect]',
621
+ basename,
622
+ '<-',
623
+ source,
624
+ 'from',
625
+ importer.slice(-80),
626
+ )
627
+ }
628
+ return target
629
+ },
596
630
  },
597
- load(id) {
598
- // synthetic package.json for react-native-worklets — see resolveId
599
- // above. just enough for upstream's version-validator to read `version`.
600
- if (id === '\0sootsim:react-native-worklets-package-json') {
631
+ load: {
632
+ filter: {
633
+ id: new RegExp(`^${virtualIdPrefix}sootsim:react-native-worklets-package-json$`),
634
+ },
635
+ handler() {
636
+ // synthetic package.json for react-native-worklets — see resolveId
637
+ // above. just enough for upstream's version-validator to read `version`.
601
638
  return `export default ${JSON.stringify({ version: '0.0.0-sootsim', name: 'react-native-worklets' })}`
602
- }
603
- return null
639
+ },
604
640
  },
605
641
  }
606
642
  }
@@ -632,42 +668,51 @@ export function manifestNativeSeamRedirect(): Plugin {
632
668
  })
633
669
  }
634
670
  }
671
+ const sourceFilter = new RegExp(
672
+ `(?:^|/)(?:${[...byPkg.values()]
673
+ .flatMap(({ basenames }) => [...basenames])
674
+ .map((basename) => basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
675
+ .join('|')})(?:\\.(?:native|web|ios|android))?$`,
676
+ )
635
677
  return {
636
678
  name: 'sootsim-manifest-native-seam-redirect',
637
679
  enforce: 'pre',
638
- resolveId(source, importer) {
639
- if (!importer || byPkg.size === 0) return null
640
- // find the package whose path appears in the importer. matches both
641
- // `/<pkg>/` (unscoped) and `/@scope/<pkg>/` (scoped — the leading `@`
642
- // is part of the pkg name in the manifest, e.g. `@notifee/react-native`).
643
- let match: { basenames: Set<string>; target: string; notes?: string } | undefined
644
- let matchedPkg: string | undefined
645
- for (const [pkg, entry] of byPkg) {
646
- if (importer.includes(`/${pkg}/`)) {
647
- match = entry
648
- matchedPkg = pkg
649
- break
680
+ resolveId: {
681
+ filter: { id: sourceFilter },
682
+ handler(source, importer) {
683
+ if (!importer || byPkg.size === 0) return null
684
+ // find the package whose path appears in the importer. matches both
685
+ // `/<pkg>/` (unscoped) and `/@scope/<pkg>/` (scoped — the leading `@`
686
+ // is part of the pkg name in the manifest, e.g. `@notifee/react-native`).
687
+ let match: { basenames: Set<string>; target: string; notes?: string } | undefined
688
+ let matchedPkg: string | undefined
689
+ for (const [pkg, entry] of byPkg) {
690
+ if (importer.includes(`/${pkg}/`)) {
691
+ match = entry
692
+ matchedPkg = pkg
693
+ break
694
+ }
650
695
  }
651
- }
652
- if (!match) return null
653
- if (importer === match.target) return null
654
- const basename = (source.split('/').pop() ?? '').replace(
655
- /\.(native|web|ios|android)$/,
656
- '',
657
- )
658
- if (!match.basenames.has(basename)) return null
659
- if (process.env.SOOTSIM_NATIVE_SEAM_DEBUG) {
660
- console.log(
661
- '[nativeSeamRedirect]',
662
- `${matchedPkg}:${basename}`,
663
- '<-',
664
- source,
665
- 'from',
666
- importer.slice(-80),
667
- match.notes ? `(${match.notes})` : '',
696
+ if (!match) return null
697
+ if (importer === match.target) return null
698
+ const basename = (source.split('/').pop() ?? '').replace(
699
+ /\.(native|web|ios|android)$/,
700
+ '',
668
701
  )
669
- }
670
- return match.target
702
+ if (!match.basenames.has(basename)) return null
703
+ if (process.env.SOOTSIM_NATIVE_SEAM_DEBUG) {
704
+ console.log(
705
+ '[nativeSeamRedirect]',
706
+ `${matchedPkg}:${basename}`,
707
+ '<-',
708
+ source,
709
+ 'from',
710
+ importer.slice(-80),
711
+ match.notes ? `(${match.notes})` : '',
712
+ )
713
+ }
714
+ return match.target
715
+ },
671
716
  },
672
717
  }
673
718
  }
@@ -689,6 +734,7 @@ export function wsBridgePlugin(): Plugin {
689
734
  let boundPort: Promise<number | null> | null = null
690
735
  return {
691
736
  name: 'sootsim-ws-bridge',
737
+ apply: 'serve',
692
738
  configureServer(server) {
693
739
  if (boundPort) return
694
740
  // lazy import to avoid pulling ws into the main bundle.
@@ -1001,67 +1047,45 @@ function sootsimConfigPlugin(
1001
1047
  // react-reconciler code. this enables runtime React version switching for
1002
1048
  // external app bundles — bindReact(appReact) swaps which React all code uses.
1003
1049
  function reactBridgePlugin(): Plugin {
1004
- let isBuild = false
1005
1050
  let redirectCount = 0
1006
- let skipCount = 0
1007
1051
  return {
1008
1052
  name: 'sootsim-react-bridge',
1053
+ apply: 'serve',
1009
1054
  enforce: 'pre',
1010
- configResolved(config) {
1011
- isBuild = config.command === 'build'
1012
- console.log('[react-bridge-plugin] isBuild:', isBuild)
1013
- },
1014
- async resolveId(source, importer, options) {
1015
- if (source !== 'react' || !importer) return null
1016
-
1017
- // log all react imports to see what's being processed
1018
- if (
1019
- importer.includes('/compat/') ||
1020
- importer.includes('/sootsim/src/') ||
1021
- importer.includes('/sootsim-engine/src/')
1022
- ) {
1023
- skipCount++
1024
- if (skipCount <= 5) {
1025
- const skip = (options as { scan?: boolean })?.scan || options?.ssr || isBuild
1026
- if (!skip) {
1027
- console.log(
1028
- '[react-bridge-plugin] react import from:',
1029
- importer.split('/').slice(-3).join('/'),
1030
- 'skip:',
1031
- skip,
1032
- )
1033
- }
1055
+ resolveId: {
1056
+ filter: { id: /^react$/ },
1057
+ async handler(_source, importer, options) {
1058
+ if (!importer) return null
1059
+
1060
+ // skip during dep scanning / SSR — only active in browser serve mode
1061
+ if ((options as { scan?: boolean })?.scan || options?.ssr) return null
1062
+
1063
+ // only redirect imports from sootsim, sootsim-engine, compat, and react-reconciler
1064
+ const shouldRedirect =
1065
+ importer.includes('/sootsim/src/') ||
1066
+ importer.includes('/sootsim-engine/src/') ||
1067
+ importer.includes('/compat/src/') ||
1068
+ importer.includes('react-reconciler')
1069
+
1070
+ if (!shouldRedirect) return null
1071
+
1072
+ // don't redirect react-bridge's own import of react (avoid infinite loop)
1073
+ if (importer.includes('react-bridge')) return null
1074
+
1075
+ // stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
1076
+ if (importer.includes('stub-registry')) return null
1077
+
1078
+ redirectCount++
1079
+ if (redirectCount <= 10) {
1080
+ console.log(
1081
+ '[react-bridge-plugin] REDIRECTING:',
1082
+ importer.split('/').slice(-3).join('/'),
1083
+ )
1084
+ } else if (redirectCount === 11) {
1085
+ console.log('[react-bridge-plugin] ... more redirects')
1034
1086
  }
1035
- }
1036
-
1037
- // skip during dep scanning / SSR — only active in browser serve mode
1038
- if ((options as { scan?: boolean })?.scan || options?.ssr || isBuild) return null
1039
-
1040
- // only redirect imports from sootsim, sootsim-engine, compat, and react-reconciler
1041
- const shouldRedirect =
1042
- importer.includes('/sootsim/src/') ||
1043
- importer.includes('/sootsim-engine/src/') ||
1044
- importer.includes('/compat/src/') ||
1045
- importer.includes('react-reconciler')
1046
-
1047
- if (!shouldRedirect) return null
1048
-
1049
- // don't redirect react-bridge's own import of react (avoid infinite loop)
1050
- if (importer.includes('react-bridge')) return null
1051
-
1052
- // stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
1053
- if (importer.includes('stub-registry')) return null
1054
-
1055
- redirectCount++
1056
- if (redirectCount <= 10) {
1057
- console.log(
1058
- '[react-bridge-plugin] REDIRECTING:',
1059
- importer.split('/').slice(-3).join('/'),
1060
- )
1061
- } else if (redirectCount === 11) {
1062
- console.log('[react-bridge-plugin] ... more redirects')
1063
- }
1064
- return { id: reactBridgePath, external: false }
1087
+ return { id: reactBridgePath, external: false }
1088
+ },
1065
1089
  },
1066
1090
  }
1067
1091
  }
@@ -1073,34 +1097,37 @@ export function workerReactBridgePlugin(): Plugin {
1073
1097
  return {
1074
1098
  name: 'sootsim-worker-react-bridge',
1075
1099
  enforce: 'pre',
1076
- async resolveId(source, importer, options) {
1077
- if (source !== 'react' || !importer) return null
1078
-
1079
- // skip during dep scanning / SSR
1080
- if ((options as { scan?: boolean })?.scan || options?.ssr) return null
1081
-
1082
- // don't redirect react-bridge's own import of react (avoid infinite loop)
1083
- if (importer.includes('react-bridge')) return null
1084
-
1085
- // stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
1086
- if (importer.includes('stub-registry')) return null
1087
-
1088
- // redirect react imports from sootsim, sootsim-engine, compat, react-reconciler,
1089
- // and any pure-JS RN-ecosystem package the bundle pulls in. without this
1090
- // redirect, packages like react-native-drawer-layout / use-latest-callback
1091
- // call into a separate React instance than the renderer/bridge does, so
1092
- // their useRef/useEffect refs collide with the engine's hooks state and
1093
- // we hit "Maximum update depth exceeded" infinite-loop renders.
1094
- const shouldRedirect =
1095
- importer.includes('/sootsim/src/') ||
1096
- importer.includes('/sootsim-engine/src/') ||
1097
- importer.includes('/compat/src/') ||
1098
- importer.includes('react-reconciler') ||
1099
- importer.includes('/node_modules/')
1100
-
1101
- if (!shouldRedirect) return null
1102
-
1103
- return { id: reactBridgePath, external: false }
1100
+ resolveId: {
1101
+ filter: { id: /^react$/ },
1102
+ async handler(_source, importer, options) {
1103
+ if (!importer) return null
1104
+
1105
+ // skip during dep scanning / SSR
1106
+ if ((options as { scan?: boolean })?.scan || options?.ssr) return null
1107
+
1108
+ // don't redirect react-bridge's own import of react (avoid infinite loop)
1109
+ if (importer.includes('react-bridge')) return null
1110
+
1111
+ // stub-registry needs the real React (with __CLIENT_INTERNALS) for bundle stubs
1112
+ if (importer.includes('stub-registry')) return null
1113
+
1114
+ // redirect react imports from sootsim, sootsim-engine, compat, react-reconciler,
1115
+ // and any pure-JS RN-ecosystem package the bundle pulls in. without this
1116
+ // redirect, packages like react-native-drawer-layout / use-latest-callback
1117
+ // call into a separate React instance than the renderer/bridge does, so
1118
+ // their useRef/useEffect refs collide with the engine's hooks state and
1119
+ // we hit "Maximum update depth exceeded" infinite-loop renders.
1120
+ const shouldRedirect =
1121
+ importer.includes('/sootsim/src/') ||
1122
+ importer.includes('/sootsim-engine/src/') ||
1123
+ importer.includes('/compat/src/') ||
1124
+ importer.includes('react-reconciler') ||
1125
+ importer.includes('/node_modules/')
1126
+
1127
+ if (!shouldRedirect) return null
1128
+
1129
+ return { id: reactBridgePath, external: false }
1130
+ },
1104
1131
  },
1105
1132
  }
1106
1133
  }
@@ -1113,8 +1140,9 @@ export function fixJsxRuntimeExports(): Plugin {
1113
1140
  return {
1114
1141
  name: 'sootsim-fix-jsx-runtime',
1115
1142
  enforce: 'pre',
1116
- async resolveId(source, importer, options) {
1117
- if (source === 'react/jsx-runtime' || source === 'react/jsx-dev-runtime') {
1143
+ resolveId: {
1144
+ filter: { id: /^react\/jsx-(?:dev-)?runtime$/ },
1145
+ async handler(source, importer, options) {
1118
1146
  const resolved = await this.resolve(source, importer, {
1119
1147
  ...options,
1120
1148
  skipSelf: true,
@@ -1125,38 +1153,42 @@ export function fixJsxRuntimeExports(): Plugin {
1125
1153
  ? '\0sootsim:jsx-runtime:'
1126
1154
  : '\0sootsim:jsx-dev-runtime:'
1127
1155
  return prefix + resolved.id
1128
- }
1129
- return null
1156
+ },
1130
1157
  },
1131
- load(id) {
1132
- if (id.startsWith('\0sootsim:jsx-runtime:')) {
1133
- const realId = id.slice('\0sootsim:jsx-runtime:'.length)
1134
- return [
1135
- `import * as _mod from ${JSON.stringify(realId)};`,
1136
- `const _m = _mod.default || _mod;`,
1137
- `export const jsx = _m.jsx || _mod.jsx;`,
1138
- `export const jsxs = _m.jsxs || _mod.jsxs;`,
1139
- `export const Fragment = _m.Fragment || _mod.Fragment;`,
1140
- ].join('\n')
1141
- }
1142
- if (id.startsWith('\0sootsim:jsx-dev-runtime:')) {
1143
- const realId = id.slice('\0sootsim:jsx-dev-runtime:'.length)
1144
- // react/jsx-runtime is already wrapped by this plugin (above) to expose
1145
- // { jsx, jsxs, Fragment } as named exports, so read them directly from
1146
- // the namespace — no `_runtime.default || _runtime` fallback needed.
1147
- // rolldown statically knows the wrapped module has no default export
1148
- // and would emit IMPORT_IS_UNDEFINED warnings on every build.
1149
- return [
1150
- `import * as _mod from ${JSON.stringify(realId)};`,
1151
- `import * as _runtime from "react/jsx-runtime";`,
1152
- `const _m = _mod.default || _mod;`,
1153
- // production React builds may not expose jsxDEV from react/jsx-dev-runtime.
1154
- // fall back to jsx so transformed modules still execute.
1155
- `export const jsxDEV = _m.jsxDEV || _mod.jsxDEV || _m.jsx || _mod.jsx || _runtime.jsx;`,
1156
- `export const Fragment = _m.Fragment || _mod.Fragment || _runtime.Fragment;`,
1157
- ].join('\n')
1158
- }
1159
- return null
1158
+ load: {
1159
+ filter: {
1160
+ id: new RegExp(`^${virtualIdPrefix}sootsim:jsx-(?:dev-)?runtime:`),
1161
+ },
1162
+ handler(id) {
1163
+ if (id.startsWith('\0sootsim:jsx-runtime:')) {
1164
+ const realId = id.slice('\0sootsim:jsx-runtime:'.length)
1165
+ return [
1166
+ `import * as _mod from ${JSON.stringify(realId)};`,
1167
+ `const _m = _mod.default || _mod;`,
1168
+ `export const jsx = _m.jsx || _mod.jsx;`,
1169
+ `export const jsxs = _m.jsxs || _mod.jsxs;`,
1170
+ `export const Fragment = _m.Fragment || _mod.Fragment;`,
1171
+ ].join('\n')
1172
+ }
1173
+ if (id.startsWith('\0sootsim:jsx-dev-runtime:')) {
1174
+ const realId = id.slice('\0sootsim:jsx-dev-runtime:'.length)
1175
+ // react/jsx-runtime is already wrapped by this plugin (above) to expose
1176
+ // { jsx, jsxs, Fragment } as named exports, so read them directly from
1177
+ // the namespace — no `_runtime.default || _runtime` fallback needed.
1178
+ // rolldown statically knows the wrapped module has no default export
1179
+ // and would emit IMPORT_IS_UNDEFINED warnings on every build.
1180
+ return [
1181
+ `import * as _mod from ${JSON.stringify(realId)};`,
1182
+ `import * as _runtime from "react/jsx-runtime";`,
1183
+ `const _m = _mod.default || _mod;`,
1184
+ // production React builds may not expose jsxDEV from react/jsx-dev-runtime.
1185
+ // fall back to jsx so transformed modules still execute.
1186
+ `export const jsxDEV = _m.jsxDEV || _mod.jsxDEV || _m.jsx || _mod.jsx || _runtime.jsx;`,
1187
+ `export const Fragment = _m.Fragment || _mod.Fragment || _runtime.Fragment;`,
1188
+ ].join('\n')
1189
+ }
1190
+ return null
1191
+ },
1160
1192
  },
1161
1193
  }
1162
1194
  }
@@ -1331,21 +1363,30 @@ export function workletsBabelTransform(
1331
1363
  return {
1332
1364
  name: 'sootsim-worklets-babel-transform',
1333
1365
  enforce: 'pre',
1334
- async transform(code, id) {
1335
- if (!shouldApplyWorkletsPlugin(id, code)) return null
1336
- try {
1337
- if (process.env.SOOTSIM_WORKLETS_BABEL_DEBUG) {
1338
- console.log('[worklets-babel] transforming', id.slice(-80))
1366
+ transform: {
1367
+ filter: {
1368
+ id: {
1369
+ include: /\.(?:tsx?|jsx?|mjs|cjs|mts|cts)$/,
1370
+ exclude: /node_modules\/(?:react|react-dom|react-native|react-native-web)\//,
1371
+ },
1372
+ code: REANIMATED_WORKLETIZATION_REGEX,
1373
+ },
1374
+ async handler(code, id) {
1375
+ if (!shouldApplyWorkletsPlugin(id, code)) return null
1376
+ try {
1377
+ if (process.env.SOOTSIM_WORKLETS_BABEL_DEBUG) {
1378
+ console.log('[worklets-babel] transforming', id.slice(-80))
1379
+ }
1380
+ return await transformWorkletsCode(code, id, true)
1381
+ } catch (err) {
1382
+ // surface but don't kill the build — fall back to raw code so the
1383
+ // rest of the pipeline can run. these failures are loud in the
1384
+ // console and easy to chase.
1385
+ const message = err instanceof Error ? err.message : String(err)
1386
+ console.warn(`[sootsim-worklets-babel] transform failed for ${id}: ${message}`)
1387
+ return null
1339
1388
  }
1340
- return await transformWorkletsCode(code, id, true)
1341
- } catch (err) {
1342
- // surface but don't kill the build — fall back to raw code so the
1343
- // rest of the pipeline can run. these failures are loud in the
1344
- // console and easy to chase.
1345
- const message = err instanceof Error ? err.message : String(err)
1346
- console.warn(`[sootsim-worklets-babel] transform failed for ${id}: ${message}`)
1347
- return null
1348
- }
1389
+ },
1349
1390
  },
1350
1391
  }
1351
1392
  }
@@ -1375,21 +1416,24 @@ function flowStripTransform(): Plugin {
1375
1416
  return {
1376
1417
  name: 'sootsim-flow-strip',
1377
1418
  enforce: 'pre',
1378
- transform(code, id) {
1379
- if (!id.includes('/node_modules/')) return null
1380
- if (!/\.[cm]?jsx?$/.test(id)) return null
1381
- if (!code.includes('@flow')) return null
1382
- loadFlowBabel()
1383
- const result = flowBabel!.transformSync(code, {
1384
- filename: id,
1385
- babelrc: false,
1386
- configFile: false,
1387
- plugins: [flowStripPlugin!, jsxSyntaxPlugin!],
1388
- sourceMaps: true,
1389
- compact: false,
1390
- })
1391
- if (!result?.code) return null
1392
- return { code: result.code, map: result.map ?? null }
1419
+ transform: {
1420
+ filter: {
1421
+ id: /\/node_modules\/.*\.[cm]?jsx?$/,
1422
+ code: /@flow/,
1423
+ },
1424
+ handler(code, id) {
1425
+ loadFlowBabel()
1426
+ const result = flowBabel!.transformSync(code, {
1427
+ filename: id,
1428
+ babelrc: false,
1429
+ configFile: false,
1430
+ plugins: [flowStripPlugin!, jsxSyntaxPlugin!],
1431
+ sourceMaps: true,
1432
+ compact: false,
1433
+ })
1434
+ if (!result?.code) return null
1435
+ return { code: result.code, map: result.map ?? null }
1436
+ },
1393
1437
  },
1394
1438
  }
1395
1439
  }
@@ -1398,20 +1442,23 @@ function nodeModulesJsxTransform(): Plugin {
1398
1442
  return {
1399
1443
  name: 'sootsim-node-modules-jsx',
1400
1444
  enforce: 'pre',
1401
- async transform(code, id) {
1402
- if (!id.includes('/node_modules/')) return null
1403
- if (!id.endsWith('.js')) return null
1404
- if (!code.includes('<')) return null
1405
- // quick check: does it look like JSX? (angle bracket followed by uppercase or known RN component)
1406
- if (!/[=(\s,]<[A-Z]/.test(code) && !/<\/[A-Z]/.test(code)) return null
1407
- const needsReact =
1408
- !code.includes('import React') && !code.includes('import * as React')
1409
- const withReact = needsReact ? `import React from 'react';\n${code}` : code
1410
- const transformed = await transformWithOxc(withReact, id, {
1411
- lang: 'jsx',
1412
- jsx: { runtime: 'classic' },
1413
- })
1414
- return { code: transformed.code, map: transformed.map }
1445
+ transform: {
1446
+ filter: {
1447
+ id: /\/node_modules\/.*\.js$/,
1448
+ code: /</,
1449
+ },
1450
+ async handler(code, id) {
1451
+ // quick check: does it look like JSX? (angle bracket followed by uppercase or known RN component)
1452
+ if (!/[=(\s,]<[A-Z]/.test(code) && !/<\/[A-Z]/.test(code)) return null
1453
+ const needsReact =
1454
+ !code.includes('import React') && !code.includes('import * as React')
1455
+ const withReact = needsReact ? `import React from 'react';\n${code}` : code
1456
+ const transformed = await transformWithOxc(withReact, id, {
1457
+ lang: 'jsx',
1458
+ jsx: { runtime: 'classic' },
1459
+ })
1460
+ return { code: transformed.code, map: transformed.map }
1461
+ },
1415
1462
  },
1416
1463
  }
1417
1464
  }
@@ -1450,29 +1497,31 @@ function metroNativeResolve(isAppSource: (p: string) => boolean): Plugin {
1450
1497
  return {
1451
1498
  name: 'sootsim-metro-native-resolve',
1452
1499
  enforce: 'pre',
1453
- resolveId(source, importer) {
1454
- if (!importer) return null
1455
- if (!importer.includes('node_modules') && !isAppSource(importer)) return null
1456
- if (!source.startsWith('.')) return null
1457
- if (platformExts.some((ext) => source.endsWith(ext))) return null
1458
-
1459
- const hasBaseExt = allExts.some((ext) => source.endsWith(ext))
1460
- const dir = path.dirname(importer)
1461
-
1462
- if (hasBaseExt) {
1463
- const resolved = path.resolve(dir, source)
1464
- for (const ext of allExts) {
1465
- if (source.endsWith(ext)) {
1466
- const found = tryResolve(resolved.slice(0, -ext.length))
1467
- if (found) return found
1468
- break
1500
+ resolveId: {
1501
+ filter: { id: /^\./ },
1502
+ handler(source, importer) {
1503
+ if (!importer) return null
1504
+ if (!importer.includes('node_modules') && !isAppSource(importer)) return null
1505
+ if (platformExts.some((ext) => source.endsWith(ext))) return null
1506
+
1507
+ const dir = path.dirname(importer)
1508
+
1509
+ const hasBaseExt = allExts.some((ext) => source.endsWith(ext))
1510
+ if (hasBaseExt) {
1511
+ const resolved = path.resolve(dir, source)
1512
+ for (const ext of allExts) {
1513
+ if (source.endsWith(ext)) {
1514
+ const found = tryResolve(resolved.slice(0, -ext.length))
1515
+ if (found) return found
1516
+ break
1517
+ }
1469
1518
  }
1519
+ } else {
1520
+ const found = tryResolve(path.resolve(dir, source))
1521
+ if (found) return found
1470
1522
  }
1471
- } else {
1472
- const found = tryResolve(path.resolve(dir, source))
1473
- if (found) return found
1474
- }
1475
- return null
1523
+ return null
1524
+ },
1476
1525
  },
1477
1526
  }
1478
1527
  }
@@ -1482,54 +1531,56 @@ function reactNativeRequirePlugin(isAppSource: (p: string) => boolean): Plugin {
1482
1531
  return {
1483
1532
  name: 'sootsim-react-native-require',
1484
1533
  enforce: 'pre',
1485
- transform(code, id) {
1486
- if (!id.includes('node_modules') && !isAppSource(id)) return null
1487
- if (!code.includes('react-native')) return null
1488
-
1489
- let hasChanges = false
1490
- let imports = ''
1491
- let result = code
1492
- let importCounter = 0
1493
-
1494
- if (
1495
- result.includes('require("react-native")') ||
1496
- result.includes("require('react-native')")
1497
- ) {
1498
- imports += `import * as __soot_rn_shim__ from ${JSON.stringify(rnShimPath)};\n`
1499
- result = result.replace(/require\(["']react-native["']\)/g, '__soot_rn_shim__')
1500
- hasChanges = true
1501
- }
1534
+ transform: {
1535
+ filter: { code: /react-native/ },
1536
+ handler(code, id) {
1537
+ if (!id.includes('node_modules') && !isAppSource(id)) return null
1538
+
1539
+ let hasChanges = false
1540
+ let imports = ''
1541
+ let result = code
1542
+ let importCounter = 0
1543
+
1544
+ if (
1545
+ result.includes('require("react-native")') ||
1546
+ result.includes("require('react-native')")
1547
+ ) {
1548
+ imports += `import * as __soot_rn_shim__ from ${JSON.stringify(rnShimPath)};\n`
1549
+ result = result.replace(/require\(["']react-native["']\)/g, '__soot_rn_shim__')
1550
+ hasChanges = true
1551
+ }
1502
1552
 
1503
- const deepRequireRegex = /require\(["'](react-native\/Libraries\/[^"']+)["']\)/g
1504
- const replacements: Array<{ full: string; path: string; varName: string }> = []
1505
- const seenPaths = new Map<string, string>()
1506
- let match
1507
-
1508
- deepRequireRegex.lastIndex = 0
1509
- while ((match = deepRequireRegex.exec(result)) !== null) {
1510
- const importPath = match[1]
1511
- if (!seenPaths.has(importPath)) {
1512
- const varName = `__soot_rn_lib_${importCounter++}__`
1513
- const stubPath = rnLibraryStubs[importPath] || rnDeepStubDefault
1514
- imports += `import * as ${varName} from ${JSON.stringify(stubPath)};\n`
1515
- seenPaths.set(importPath, varName)
1553
+ const deepRequireRegex = /require\(["'](react-native\/Libraries\/[^"']+)["']\)/g
1554
+ const replacements: Array<{ full: string; path: string; varName: string }> = []
1555
+ const seenPaths = new Map<string, string>()
1556
+ let match
1557
+
1558
+ deepRequireRegex.lastIndex = 0
1559
+ while ((match = deepRequireRegex.exec(result)) !== null) {
1560
+ const importPath = match[1]
1561
+ if (!seenPaths.has(importPath)) {
1562
+ const varName = `__soot_rn_lib_${importCounter++}__`
1563
+ const stubPath = rnLibraryStubs[importPath] || rnDeepStubDefault
1564
+ imports += `import * as ${varName} from ${JSON.stringify(stubPath)};\n`
1565
+ seenPaths.set(importPath, varName)
1566
+ }
1567
+ replacements.push({
1568
+ full: match[0],
1569
+ path: importPath,
1570
+ varName: seenPaths.get(importPath)!,
1571
+ })
1516
1572
  }
1517
- replacements.push({
1518
- full: match[0],
1519
- path: importPath,
1520
- varName: seenPaths.get(importPath)!,
1521
- })
1522
- }
1523
1573
 
1524
- if (replacements.length > 0) {
1525
- for (const rep of replacements) {
1526
- result = result.replace(rep.full, rep.varName)
1574
+ if (replacements.length > 0) {
1575
+ for (const rep of replacements) {
1576
+ result = result.replace(rep.full, rep.varName)
1577
+ }
1578
+ hasChanges = true
1527
1579
  }
1528
- hasChanges = true
1529
- }
1530
1580
 
1531
- if (!hasChanges) return null
1532
- return { code: imports + result, map: null }
1581
+ if (!hasChanges) return null
1582
+ return { code: imports + result, map: null }
1583
+ },
1533
1584
  },
1534
1585
  }
1535
1586
  }
@@ -1540,8 +1591,6 @@ function externalAppResolvePlugin(
1540
1591
  isAppSource: (p: string) => boolean,
1541
1592
  ownedPackages: Set<string>,
1542
1593
  ): Plugin {
1543
- if (!appDir) return { name: 'sootsim-external-app-resolve' }
1544
-
1545
1594
  // synthetic importer inside the app dir so vite searches app's node_modules
1546
1595
  // using its conditions-aware exports resolution (not CJS require.resolve)
1547
1596
  const appVirtualImporter = path.join(appDir, '_virtual_.js')
@@ -1574,56 +1623,60 @@ function externalAppTransformPlugin(isAppSource: (p: string) => boolean): Plugin
1574
1623
  return {
1575
1624
  name: 'sootsim-external-app-transform',
1576
1625
  enforce: 'pre',
1577
- async transform(code, id) {
1578
- if (!isAppSource(id)) return null
1579
-
1580
- let result = code
1581
- let imports = ''
1582
- let importCounter = 0
1583
- let hasChanges = false
1584
-
1585
- if (code.includes('require(')) {
1586
- const requireRegex = /require\(["']([^"']+)["']\)/g
1587
- const replacements: Array<{ full: string; varName: string }> = []
1588
- const seenPaths = new Map<string, string>()
1589
- let match
1590
-
1591
- requireRegex.lastIndex = 0
1592
- while ((match = requireRegex.exec(result)) !== null) {
1593
- const reqPath = match[1]
1594
- if (reqPath === 'react-native' || reqPath.startsWith('react-native/')) continue
1595
-
1596
- if (!seenPaths.has(reqPath)) {
1597
- const varName = `__soot_cjs_${importCounter++}__`
1598
- imports += `import * as ${varName} from ${JSON.stringify(reqPath)};\n`
1599
- seenPaths.set(reqPath, varName)
1626
+ transform: {
1627
+ filter: { code: /require\(|</ },
1628
+ async handler(code, id) {
1629
+ if (!isAppSource(id)) return null
1630
+
1631
+ let result = code
1632
+ let imports = ''
1633
+ let importCounter = 0
1634
+ let hasChanges = false
1635
+
1636
+ if (code.includes('require(')) {
1637
+ const requireRegex = /require\(["']([^"']+)["']\)/g
1638
+ const replacements: Array<{ full: string; varName: string }> = []
1639
+ const seenPaths = new Map<string, string>()
1640
+ let match
1641
+
1642
+ requireRegex.lastIndex = 0
1643
+ while ((match = requireRegex.exec(result)) !== null) {
1644
+ const reqPath = match[1]
1645
+ if (reqPath === 'react-native' || reqPath.startsWith('react-native/'))
1646
+ continue
1647
+
1648
+ if (!seenPaths.has(reqPath)) {
1649
+ const varName = `__soot_cjs_${importCounter++}__`
1650
+ imports += `import * as ${varName} from ${JSON.stringify(reqPath)};\n`
1651
+ seenPaths.set(reqPath, varName)
1652
+ }
1653
+ replacements.push({ full: match[0], varName: seenPaths.get(reqPath)! })
1600
1654
  }
1601
- replacements.push({ full: match[0], varName: seenPaths.get(reqPath)! })
1602
- }
1603
1655
 
1604
- if (replacements.length > 0) {
1605
- for (const rep of replacements) {
1606
- result = result.replace(rep.full, rep.varName)
1656
+ if (replacements.length > 0) {
1657
+ for (const rep of replacements) {
1658
+ result = result.replace(rep.full, rep.varName)
1659
+ }
1660
+ hasChanges = true
1607
1661
  }
1608
- hasChanges = true
1609
1662
  }
1610
- }
1611
1663
 
1612
- if (imports) result = imports + result
1664
+ if (imports) result = imports + result
1613
1665
 
1614
- if (id.endsWith('.js') && result.includes('<')) {
1615
- const needsReact =
1616
- !result.includes('import React') && !result.includes('import * as React')
1617
- const withReact = needsReact ? `import React from 'react';\n${result}` : result
1618
- const transformed = await transformWithOxc(withReact, id, {
1619
- lang: 'jsx',
1620
- jsx: { runtime: 'classic' },
1621
- })
1622
- return { code: transformed.code, map: transformed.map }
1623
- }
1666
+ if (id.endsWith('.js') && result.includes('<')) {
1667
+ const needsReact =
1668
+ !result.includes('import React') && !result.includes('import * as React')
1669
+ const withReact = needsReact ? `import React from 'react';\n${result}` : result
1670
+ const transformed = await transformWithOxc(withReact, id, {
1671
+ lang: 'jsx',
1672
+ jsx: { runtime: 'classic' },
1673
+ })
1674
+ return { code: transformed.code, map: transformed.map }
1675
+ }
1624
1676
 
1625
- if (!hasChanges) return null
1626
- return { code: result, map: null }
1677
+ if (!hasChanges) return null
1678
+ return { code: result, map: null }
1679
+ },
1627
1680
  },
1628
1681
  }
1629
1682
  }
@@ -1658,17 +1711,20 @@ function stubMissingImages(isAppSource: (p: string) => boolean): Plugin {
1658
1711
  return {
1659
1712
  name: 'sootsim-stub-images',
1660
1713
  enforce: 'pre',
1661
- resolveId(id, importer) {
1662
- if (id.endsWith('.jpg') || id.endsWith('.png') || id.endsWith('.gif')) {
1714
+ resolveId: {
1715
+ filter: { id: /\.(?:jpg|png|gif)$/ },
1716
+ handler(id, importer) {
1663
1717
  if (importer?.includes('node_modules') || (importer && isAppSource(importer))) {
1664
1718
  return '\0stub-image'
1665
1719
  }
1666
- }
1667
- return null
1720
+ return null
1721
+ },
1668
1722
  },
1669
- load(id) {
1670
- if (id === '\0stub-image') return 'export default ""'
1671
- return null
1723
+ load: {
1724
+ filter: { id: new RegExp(`^${virtualIdPrefix}stub-image$`) },
1725
+ handler() {
1726
+ return 'export default ""'
1727
+ },
1672
1728
  },
1673
1729
  }
1674
1730
  }