vxrn 1.25.16 → 1.26.0

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 (72) hide show
  1. package/dist/index.js +2 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +2 -0
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/index.native.js +2 -0
  6. package/dist/index.native.js.map +1 -1
  7. package/dist/patches/builtInDepPatches.mjs +21 -0
  8. package/dist/patches/builtInDepPatches.mjs.map +1 -1
  9. package/dist/patches/builtInDepPatches.native.js +21 -0
  10. package/dist/patches/builtInDepPatches.native.js.map +1 -1
  11. package/dist/plugins/reactNativeDevServer.mjs +5 -0
  12. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  13. package/dist/plugins/reactNativeDevServer.native.js +5 -0
  14. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  15. package/dist/plugins/rnCodegenPlugin.mjs +16 -0
  16. package/dist/plugins/rnCodegenPlugin.mjs.map +1 -0
  17. package/dist/plugins/rnCodegenPlugin.native.js +16 -0
  18. package/dist/plugins/rnCodegenPlugin.native.js.map +1 -0
  19. package/dist/plugins/rnCodegenPlugin.test.mjs +239 -0
  20. package/dist/plugins/rnCodegenPlugin.test.mjs.map +1 -0
  21. package/dist/plugins/rnCodegenPlugin.test.native.js +242 -0
  22. package/dist/plugins/rnCodegenPlugin.test.native.js.map +1 -0
  23. package/dist/plugins/workletImportsPlugin.mjs +160 -0
  24. package/dist/plugins/workletImportsPlugin.mjs.map +1 -0
  25. package/dist/plugins/workletImportsPlugin.native.js +230 -0
  26. package/dist/plugins/workletImportsPlugin.native.js.map +1 -0
  27. package/dist/utils/createNativeDevEngine.mjs +198 -92
  28. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  29. package/dist/utils/createNativeDevEngine.native.js +252 -99
  30. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  31. package/dist/utils/createNativeDevEngine.test.mjs +775 -4
  32. package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
  33. package/dist/utils/createNativeDevEngine.test.native.js +836 -4
  34. package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
  35. package/dist/utils/patches.mjs +12 -5
  36. package/dist/utils/patches.mjs.map +1 -1
  37. package/dist/utils/patches.native.js +13 -5
  38. package/dist/utils/patches.native.js.map +1 -1
  39. package/dist/utils/patches.test.mjs +28 -0
  40. package/dist/utils/patches.test.mjs.map +1 -1
  41. package/dist/utils/patches.test.native.js +47 -0
  42. package/dist/utils/patches.test.native.js.map +1 -1
  43. package/dist/utils/scanDepsToOptimize.mjs +5 -1
  44. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  45. package/dist/utils/scanDepsToOptimize.native.js +5 -1
  46. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  47. package/package.json +19 -12
  48. package/src/index.ts +2 -0
  49. package/src/patches/builtInDepPatches.ts +25 -0
  50. package/src/plugins/reactNativeDevServer.ts +12 -0
  51. package/src/plugins/rnCodegenPlugin.test.ts +313 -0
  52. package/src/plugins/rnCodegenPlugin.ts +20 -0
  53. package/src/plugins/workletImportsPlugin.ts +226 -0
  54. package/src/utils/createNativeDevEngine.test.ts +877 -3
  55. package/src/utils/createNativeDevEngine.ts +259 -107
  56. package/src/utils/patches.test.ts +33 -0
  57. package/src/utils/patches.ts +17 -6
  58. package/src/utils/scanDepsToOptimize.ts +9 -0
  59. package/types/index.d.ts +2 -0
  60. package/types/index.d.ts.map +1 -1
  61. package/types/patches/builtInDepPatches.d.ts.map +1 -1
  62. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  63. package/types/plugins/rnCodegenPlugin.d.ts +6 -0
  64. package/types/plugins/rnCodegenPlugin.d.ts.map +1 -0
  65. package/types/plugins/rnCodegenPlugin.test.d.ts +2 -0
  66. package/types/plugins/rnCodegenPlugin.test.d.ts.map +1 -0
  67. package/types/plugins/workletImportsPlugin.d.ts +8 -0
  68. package/types/plugins/workletImportsPlugin.d.ts.map +1 -0
  69. package/types/utils/createNativeDevEngine.d.ts +13 -0
  70. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  71. package/types/utils/patches.d.ts.map +1 -1
  72. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
@@ -20,11 +20,10 @@ import { pathToFileURL } from 'node:url'
20
20
  import type { InputOptions, OutputOptions, Plugin, RolldownOutput } from 'rolldown'
21
21
  import type { DevEngine } from 'rolldown/experimental'
22
22
  import { loadEnv as loadViteEnv, normalizePath } from 'vite'
23
+ import { shouldStripFlow, transformHermesAsync } from '@vxrn/compiler'
23
24
  import { DEFAULT_ASSET_EXTS } from '../constants/defaults'
24
25
  import { getNativePrelude } from '../runtime/native-prelude'
25
-
26
- // files that contain Flow syntax and need stripping
27
- const FLOW_FILE_PATTERN = /node_modules[\\/](?:react-native|@react-native)[\\/].*\.js$/
26
+ import { rnCodegenPlugin } from '../plugins/rnCodegenPlugin'
28
27
 
29
28
  // Hermes needs the whole class shape lowered *together*. downleveling only the
30
29
  // class fields while leaving `class ... extends` as modern ES6 produces a
@@ -136,6 +135,16 @@ export function getNativeTransformConfig(
136
135
  }
137
136
  })()
138
137
 
138
+ // app-supplied defines (One's native.bundlerOptions.define). vite's rule:
139
+ // a string is a raw expression, anything else is JSON stringified. rolldown's
140
+ // binding only accepts strings, so normalize rather than crashing the build.
141
+ const userDefines: Record<string, string> = {}
142
+ for (const [key, value] of Object.entries(
143
+ ((globalThis as any).__vxrnNativeUserDefine || {}) as Record<string, unknown>
144
+ )) {
145
+ userDefines[key] = typeof value === 'string' ? value : JSON.stringify(value)
146
+ }
147
+
139
148
  const mode = dev ? 'development' : 'production'
140
149
 
141
150
  // Match One's Vite client contract: load public values from process.env and
@@ -174,6 +183,9 @@ export function getNativeTransformConfig(
174
183
  runtime: 'classic' as const,
175
184
  },
176
185
  define: {
186
+ // first in the map, so nothing the user set can shadow a platform-owned
187
+ // key below.
188
+ ...userDefines,
177
189
  // Public values are applied first so platform-owned keys cannot inherit
178
190
  // the SSR values used while loading One's Vite config.
179
191
  ...envDefines,
@@ -237,6 +249,8 @@ function getNativePlugins(
237
249
  // type argument intact. stripping Flow first would erase it (which is why the
238
250
  // codegen "didn't run for <Component>" warning fired).
239
251
  vxrnCompilerPlugin(platform, dev, root, sourceMaps),
252
+ // react-native codegen: transforms TurboModule / Fabric specs before Flow stripping
253
+ rnCodegenPlugin({ projectRoot: root }),
240
254
  // strip Flow from any react-native / @react-native `.js` the compiler didn't
241
255
  // handle, the guaranteed safety net before rolldown's oxc core parse (which
242
256
  // can't parse Flow). now downstream of the compiler, so codegen sees the types.
@@ -247,6 +261,9 @@ function getNativePlugins(
247
261
  assetPlugin({ root, platform, assetsDest, onAsset }),
248
262
  // hermes compat: transform class properties and private fields
249
263
  hermesCompatSWCPlugin(dev, sourceMaps),
264
+ // hermes compat: per-iteration loop bindings. runs last so it also covers
265
+ // loops the earlier lowering steps emit.
266
+ hermesLoopsPlugin(sourceMaps),
250
267
  ]
251
268
  }
252
269
 
@@ -268,12 +285,10 @@ function getNativeOutputOptions(prelude: string, sourcemap: boolean): OutputOpti
268
285
  * - DevSettings stripping → stripDevSettingsPlugin
269
286
  */
270
287
  export function normalizeNativeCommonJSInterop(code: string): string {
271
- // Rolldown can mark ESM default imports from internal CommonJS modules as
272
- // Node-mode conversions in both dev and production. Babel CommonJS packages
273
- // expose their actual default behind `exports.default`; Node mode instead
274
- // returns the whole exports object and React receives `{ default: Component }`.
288
+ // native packages expose Babel defaults through exports.default. apply the
289
+ // same interop to bundled require helpers and HMR's runtime export lookups.
275
290
  return code.replace(
276
- /(\b__toESM(?:\$\d+)?\(\s*require[\w$]*\(\)\s*),\s*1(\s*\))/g,
291
+ /(\b__toESM(?:\$\d+)?\(\s*(?:require[\w$]*\(\)|__rolldown_runtime__\.loadExports\("(?:\\.|[^"\\])*"\))\s*),\s*1(\s*\))/g,
277
292
  '$1$2'
278
293
  )
279
294
  }
@@ -373,34 +388,16 @@ async function downlevelClassFieldsInBundle(code: string): Promise<string> {
373
388
  const runtimeSection = code.slice(startIdx, runtimeEnd)
374
389
  const originalNewlines = runtimeSection.match(/\n/g)?.length ?? 0
375
390
 
376
- const swc = await import('@swc/core')
377
- const result = await swc.transform(runtimeSection, {
378
- filename: 'rolldown-runtime.js',
379
- configFile: false,
380
- swcrc: false,
381
- sourceMaps: false,
382
- inputSourceMap: false,
383
- isModule: false,
384
- // Compact only Rolldown's generated runtime, then restore its original
385
- // newline count below. This keeps every application module at the exact
386
- // generated line recorded by Rolldown's source map while still lowering
387
- // runtime class fields for Hermes.
388
- minify: true,
389
- env: {
390
- targets: { node: 9999 },
391
- // dev-only runtime prelude: the class set only, no prod bytecode transforms
392
- include: [...HERMES_CLASS_TRANSFORMS],
393
- },
394
- jsc: {
395
- parser: { syntax: 'ecmascript' },
396
- transform: { react: { runtime: 'preserve' } },
397
- externalHelpers: false,
398
- assumptions: {
399
- setPublicClassFields: true,
400
- privateFieldsAsProperties: true,
401
- },
391
+ const { transformSync } = await import('oxc-transform')
392
+ const result = transformSync('rolldown-runtime.js', runtimeSection, {
393
+ target: 'es2020',
394
+ assumptions: {
395
+ setPublicClassFields: true,
402
396
  },
403
397
  })
398
+ if (result.errors?.length) {
399
+ throw new Error(result.errors.map((e) => e.message).join('\n'))
400
+ }
404
401
  const transformedCode = result.code.trimEnd()
405
402
  // SWC minification strips comments, but the dev bundle scope wrapper uses
406
403
  // this generated-runtime marker as its structural boundary.
@@ -585,7 +582,7 @@ try {
585
582
  onHmrUpdate?.({
586
583
  type: 'hmr:update',
587
584
  clientId,
588
- code: update.code,
585
+ code: normalizeNativeCommonJSInterop(update.code),
589
586
  changedIds: update.changedIds,
590
587
  seq: update.seq,
591
588
  })
@@ -1018,50 +1015,150 @@ export function vxrnCompilerPlugin(
1018
1015
 
1019
1016
  let babelOptions = compiler.getBabelOptions(props)
1020
1017
 
1021
- if (needsRefresh) {
1022
- // merge react-refresh/babel into the existing plugins (or create new options)
1023
- const existingPlugins = babelOptions?.plugins || []
1024
- babelOptions = {
1025
- ...babelOptions,
1026
- plugins: [
1027
- ...existingPlugins,
1028
- [
1029
- 'react-refresh/babel',
1030
- {
1031
- skipEnvCheck: true,
1032
- refreshReg: '__vxrnRefreshReg',
1033
- refreshSig: '__vxrnRefreshSig',
1034
- },
1035
- ],
1036
- ],
1018
+ let curCode = code
1019
+ const intermediateMaps: any[] = []
1020
+
1021
+ const isWorkletPlugin = (entry: any) => {
1022
+ if (!entry) return false
1023
+ const name =
1024
+ typeof entry === 'string' ? entry : Array.isArray(entry) ? entry[0] : null
1025
+ return (
1026
+ typeof name === 'string' &&
1027
+ (name.includes('react-native-worklets') ||
1028
+ name.includes('react-native-reanimated'))
1029
+ )
1030
+ }
1031
+ const workletEntry = babelOptions?.plugins?.find(isWorkletPlugin)
1032
+ const workletPluginOptions = Array.isArray(workletEntry) ? workletEntry[1] : undefined
1033
+ const useWorklets =
1034
+ compiler.isNativeWorkletsEnabled() &&
1035
+ (Boolean(workletEntry) || compiler.shouldTransformWorklets({ id, code }))
1036
+ const compilerPluginIndex =
1037
+ babelOptions?.plugins?.findIndex(
1038
+ (x) => Array.isArray(x) && x[0] === 'babel-plugin-react-compiler'
1039
+ ) ?? -1
1040
+
1041
+ if (compilerPluginIndex !== -1) {
1042
+ // preserve automatic worklet candidates before react compiler hoists them.
1043
+ if (useWorklets) {
1044
+ const prepared = compiler.prepareWorkletsForReactCompiler(id, curCode, sourceMaps)
1045
+ if (prepared) {
1046
+ curCode = prepared.code
1047
+ if (prepared.map) intermediateMaps.push(prepared.map)
1048
+ }
1049
+ }
1050
+ const compilerTarget =
1051
+ (babelOptions!.plugins![compilerPluginIndex] as any[])[1]?.target ?? '19'
1052
+ const compilerOut = await compiler.transformOxcReactCompiler(
1053
+ id, curCode, compilerTarget, sourceMaps
1054
+ )
1055
+ if (compilerOut?.code) {
1056
+ curCode = compilerOut.code
1057
+ if (sourceMaps && compilerOut.map) intermediateMaps.push(compilerOut.map)
1058
+ }
1059
+ babelOptions!.plugins!.splice(compilerPluginIndex, 1)
1060
+ if (babelOptions!.plugins!.length === 0) babelOptions = null
1061
+ }
1062
+
1063
+ if (useWorklets) {
1064
+ const workletOut = await compiler.transformWorklets(id, curCode, sourceMaps, {
1065
+ projectRoot,
1066
+ ...workletPluginOptions,
1067
+ })
1068
+ if (workletOut?.code) {
1069
+ curCode = workletOut.code
1070
+ if (sourceMaps && workletOut.map) intermediateMaps.push(workletOut.map)
1071
+ }
1072
+ if (babelOptions?.plugins) {
1073
+ babelOptions.plugins = babelOptions.plugins.filter(
1074
+ (entry) => !isWorkletPlugin(entry)
1075
+ )
1076
+ if (babelOptions.plugins.length === 0) babelOptions = null
1037
1077
  }
1038
1078
  }
1039
1079
 
1040
- if (!babelOptions) return
1080
+ if (!babelOptions && !needsRefresh) {
1081
+ if (curCode !== code) {
1082
+ let finalMap: any = undefined
1083
+ if (sourceMaps && intermediateMaps.length > 0) {
1084
+ if (intermediateMaps.length === 1) {
1085
+ finalMap = intermediateMaps[0]
1086
+ } else {
1087
+ const remapping = (await import('@jridgewell/remapping')).default
1088
+ finalMap = remapping(intermediateMaps.slice().reverse(), () => null)
1089
+ }
1090
+ }
1091
+ return { code: curCode, map: finalMap }
1092
+ }
1093
+ return
1094
+ }
1041
1095
 
1042
- if (sourceMaps) {
1043
- // Rolldown composes plugin maps in transform order. Without this map,
1044
- // every module Babel changes is attributed only to Babel's generated
1045
- // output, which makes production Hermes frames miss the original
1046
- // source line even though the bundle command emits a `.map` file.
1096
+ if (sourceMaps && babelOptions) {
1097
+ // Individual stage maps are collected in intermediateMaps and composed
1098
+ // once via remapping([wrapMap, refreshMap, ...intermediateMaps.reverse()]).
1099
+ // Do not pass intermediateMaps as inputSourceMap to Babel, which would
1100
+ // cause Babel to pre-compose and remapping to double-compose the compiler map.
1047
1101
  babelOptions = {
1048
1102
  ...babelOptions,
1049
1103
  sourceMaps: true,
1050
1104
  sourceFileName: id,
1051
1105
  }
1106
+ delete (babelOptions as any).inputSourceMap
1052
1107
  }
1053
1108
 
1054
- const result = await compiler.transformBabel(id, code, babelOptions)
1109
+ if (babelOptions) {
1110
+ const result = await compiler.transformBabel(id, curCode, babelOptions)
1111
+ if (result?.code) {
1112
+ curCode = result.code
1113
+ if (sourceMaps && result.map) {
1114
+ intermediateMaps.push(result.map)
1115
+ }
1116
+ if (!needsRefresh) {
1117
+ let finalMap: any = undefined
1118
+ if (sourceMaps && intermediateMaps.length > 0) {
1119
+ if (intermediateMaps.length === 1) {
1120
+ finalMap = intermediateMaps[0]
1121
+ } else {
1122
+ const remapping = (await import('@jridgewell/remapping')).default
1123
+ finalMap = remapping(intermediateMaps.slice().reverse(), () => null)
1124
+ }
1125
+ }
1126
+ return { code: curCode, map: finalMap }
1127
+ }
1128
+ }
1129
+ }
1055
1130
 
1056
- if (result?.code) {
1057
- let out = result.code
1131
+ if (needsRefresh) {
1132
+ const { transformSync } = await import('oxc-transform-react')
1133
+ const jsxImportSource =
1134
+ compiler.configuration?.enableNativewind && !id.includes('node_modules')
1135
+ ? 'nativewind'
1136
+ : 'react'
1137
+
1138
+ const res = transformSync(id, curCode, {
1139
+ jsx: {
1140
+ development: dev,
1141
+ runtime: 'automatic',
1142
+ importSource: jsxImportSource,
1143
+ refresh: {
1144
+ refreshReg: '__vxrnRefreshReg',
1145
+ refreshSig: '__vxrnRefreshSig',
1146
+ },
1147
+ },
1148
+ reactCompiler: false,
1149
+ sourcemap: sourceMaps,
1150
+ })
1058
1151
 
1059
- if (needsRefresh) {
1060
- // wrap with per-file $RefreshReg$ that includes the file path as unique ID
1061
- // and schedule performReactRefresh() after HMR patch re-execution
1062
- const escapedId = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
1063
- out = `
1064
- var __prevRefreshReg = globalThis.$RefreshReg$;
1152
+ if (res.fatal) {
1153
+ throw new Error(
1154
+ `[vxrn:compiler] oxc react transform failed on ${id}: ${(res.errors ?? [])
1155
+ .map((e: any) => e.message ?? String(e))
1156
+ .join(', ')}`
1157
+ )
1158
+ }
1159
+
1160
+ const escapedId = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
1161
+ const header = `var __prevRefreshReg = globalThis.$RefreshReg$;
1065
1162
  var __prevRefreshSig = globalThis.$RefreshSig$;
1066
1163
  if (globalThis.__ReactRefresh) {
1067
1164
  globalThis.$RefreshReg$ = function(type, id) {
@@ -1073,7 +1170,8 @@ if (globalThis.__ReactRefresh) {
1073
1170
  var __vxrnRefreshReg = globalThis.$RefreshReg$;
1074
1171
  var __vxrnRefreshSig = globalThis.$RefreshSig$;
1075
1172
 
1076
- ${out}
1173
+ `
1174
+ const footer = `
1077
1175
 
1078
1176
  globalThis.$RefreshReg$ = __prevRefreshReg;
1079
1177
  globalThis.$RefreshSig$ = __prevRefreshSig;
@@ -1085,24 +1183,48 @@ if (import.meta.hot) {
1085
1183
  });
1086
1184
  }
1087
1185
  `
1186
+ let out: string
1187
+ let finalMap: any = undefined
1188
+
1189
+ if (sourceMaps) {
1190
+ const MagicString = (await import('magic-string')).default
1191
+ const ms = new MagicString(res.code)
1192
+ ms.prepend(header)
1193
+ ms.append(footer)
1194
+ out = ms.toString()
1195
+ const wrapMap = ms.generateMap({ source: id, hires: true })
1196
+
1197
+ const allMaps = [
1198
+ wrapMap,
1199
+ res.map,
1200
+ ...intermediateMaps.slice().reverse(),
1201
+ ].filter(Boolean)
1202
+ const remapping = (await import('@jridgewell/remapping')).default
1203
+ finalMap = remapping(allMaps, () => null)
1204
+ } else {
1205
+ out = header + res.code + footer
1088
1206
  }
1089
1207
 
1090
- return { code: out, map: sourceMaps ? result.map : undefined }
1208
+ return { code: out, map: finalMap }
1091
1209
  }
1092
1210
  },
1093
1211
  }
1094
1212
  }
1095
1213
 
1096
1214
  /**
1097
- * Strip Flow types from react-native source files.
1098
- * Uses hermes-parser which is already a dep of react-native.
1215
+ * Strip Flow types from react-native and other Flow-authored source files.
1216
+ *
1217
+ * The `.vxrn.original` patch step rewrites some of these packages in
1218
+ * node_modules ahead of time, but that step is best-effort and skips a module
1219
+ * whose transform throws. Deciding here on the file's own contents means the
1220
+ * bundle no longer depends on that mutation having succeeded.
1099
1221
  */
1100
1222
  function flowStripPlugin(): Plugin {
1101
1223
  return {
1102
1224
  name: 'vxrn:flow-strip',
1103
1225
  transform: {
1104
1226
  async handler(code, id) {
1105
- if (!FLOW_FILE_PATTERN.test(id)) return
1227
+ if (!shouldStripFlow(id, code)) return
1106
1228
 
1107
1229
  const fft = await import('fast-flow-transform')
1108
1230
  const result = await fft.default({
@@ -1389,7 +1511,7 @@ function copyNativeAssetFiles(
1389
1511
  * Inspired by rollipop's swc-plugin.ts.
1390
1512
  */
1391
1513
  export function hermesCompatSWCPlugin(dev: boolean, sourceMaps = false): Plugin {
1392
- let swc: typeof import('@swc/core') | null = null
1514
+ let oxc: typeof import('oxc-transform') | null = null
1393
1515
 
1394
1516
  return {
1395
1517
  name: 'vxrn:hermes-compat',
@@ -1398,43 +1520,73 @@ export function hermesCompatSWCPlugin(dev: boolean, sourceMaps = false): Plugin
1398
1520
  if (id.includes('\0') || id.includes('virtual:')) return
1399
1521
  // skip files that don't need transformation
1400
1522
  const hasClass = code.includes('class ') || code.includes('class{')
1401
- const hasAsync = code.includes('async ')
1523
+ const hasAsync = code.includes('async')
1402
1524
  const hasBlockScopedLoop = /\bfor\s*\(\s*(?:const|let)\b/.test(code)
1403
1525
  if (!hasClass && !hasAsync && !hasBlockScopedLoop) return
1404
- // skip very large prebuilt files
1405
- if (code.length > 500_000) return
1406
-
1407
- if (!swc) swc = await import('@swc/core')
1408
-
1409
- // app modules: the Hermes class and async sets in both modes.
1410
- const envIncludes = getHermesSWCIncludes(dev)
1411
-
1412
- const result = await swc.transform(code, {
1413
- filename: id,
1414
- configFile: false,
1415
- swcrc: false,
1416
- // Return the transform-to-input map and let Rolldown compose it with
1417
- // earlier plugin maps. `inputSourceMap` stays false deliberately:
1418
- // feeding the prior map to SWC as well would compose it twice.
1419
- sourceMaps,
1420
- sourceFileName: sourceMaps ? id : undefined,
1421
- inputSourceMap: false,
1422
- env: {
1423
- targets: { node: 9999 },
1424
- include: envIncludes,
1425
- },
1426
- jsc: {
1427
- parser: { syntax: 'typescript', tsx: true },
1428
- transform: { react: { runtime: 'preserve' } },
1429
- externalHelpers: false,
1430
- assumptions: {
1431
- setPublicClassFields: true,
1432
- privateFieldsAsProperties: true,
1433
- },
1434
- },
1435
- isModule: !id.endsWith('.cjs'),
1436
- })
1437
- return { code: result.code, map: sourceMaps ? result.map : undefined }
1526
+ let output: { code: string; map?: any } | undefined
1527
+ // keep the existing oxc limit for large prebuilt files. async lowering
1528
+ // below has no size exemption and runs after the worklet transform.
1529
+ if (code.length <= 500_000) {
1530
+ if (!oxc) oxc = await import('oxc-transform')
1531
+ const lang = /\.[cm]?ts$/.test(id) ? 'ts' : id.endsWith('.tsx') ? 'tsx' : 'jsx'
1532
+ const result = oxc.transformSync(id, code, {
1533
+ lang,
1534
+ target: 'es2020',
1535
+ assumptions: { setPublicClassFields: true },
1536
+ jsx: 'preserve',
1537
+ sourcemap: sourceMaps,
1538
+ sourceType: id.endsWith('.cjs') ? 'script' : 'module',
1539
+ })
1540
+ if (result.errors?.length) {
1541
+ const err = result.errors[0]
1542
+ throw new Error(err.message + (err.codeframe ? `\n${err.codeframe}` : ''))
1543
+ }
1544
+ output = { code: result.code, map: sourceMaps ? result.map : undefined }
1545
+ }
1546
+ const lowered = await transformHermesAsync(output?.code ?? code, id, sourceMaps)
1547
+ if (!lowered) return output
1548
+ if (lowered.map && output?.map) {
1549
+ const remapping = (await import('@jridgewell/remapping')).default
1550
+ lowered.map = remapping([lowered.map, output.map], () => null)
1551
+ }
1552
+ return lowered
1553
+ },
1554
+ }
1555
+ }
1556
+
1557
+ export const hermesCompatPlugin = hermesCompatSWCPlugin
1558
+
1559
+ /**
1560
+ * Hermes gives a loop one environment, not one per iteration, so every closure
1561
+ * created in a loop body sees the binding's final value. zod installs its schema
1562
+ * methods with `for (const key in methods) defineProperty(proto, key, {get(){...}})`,
1563
+ * and without this every method on every zod schema resolved to the last one:
1564
+ * `string().nullish()` called `apply` and the app red-screened at startup.
1565
+ * See @vxrn/compiler's transformHermesLoops for the bytecode evidence.
1566
+ *
1567
+ * Rolldown's own interop helpers are emitted after this runs, but they are
1568
+ * already `var`-based with `.bind(null, key)`, so they need no rewriting.
1569
+ */
1570
+ export function hermesLoopsPlugin(sourceMaps = false): Plugin {
1571
+ let compiler: typeof import('@vxrn/compiler') | null = null
1572
+
1573
+ return {
1574
+ name: 'vxrn:hermes-loops',
1575
+ async transform(code, id) {
1576
+ if (!/\.[cm]?[jt]sx?$/.test(id)) return
1577
+ if (id.includes('\0') || id.includes('virtual:')) return
1578
+
1579
+ if (!compiler) compiler = await import('@vxrn/compiler')
1580
+ const result = compiler.transformHermesLoops(code, id)
1581
+ if (!result) return
1582
+
1583
+ if (!sourceMaps) return { code: result.code, map: null }
1584
+ if (result.maps.length === 1) return { code: result.code, map: result.maps[0] as any }
1585
+ const remapping = (await import('@jridgewell/remapping')).default
1586
+ return {
1587
+ code: result.code,
1588
+ map: remapping(result.maps.slice().reverse(), () => null) as any,
1589
+ }
1438
1590
  },
1439
1591
  }
1440
1592
  }
@@ -10,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
10
10
  vi.mock('@vxrn/compiler', () => ({ transformSWC: vi.fn() }))
11
11
  vi.mock('@vxrn/vite-flow', () => ({ transformFlowBabel: vi.fn() }))
12
12
 
13
+ import { transformSWC } from '@vxrn/compiler'
13
14
  import {
14
15
  type DepPatch,
15
16
  applyDependencyPatches,
@@ -336,4 +337,36 @@ describe('applyDependencyPatches', () => {
336
337
  expect(warn.mock.calls.flat().join(' ')).not.toContain('stale')
337
338
  warn.mockRestore()
338
339
  })
340
+
341
+ // a patch is written to node_modules once and then read by every later build,
342
+ // so it must be transformed as production. the dev JSX runtime emits `jsxDEV`,
343
+ // which React's production build exports as `void 0`, so a patched dependency
344
+ // rendering JSX threw "undefined is not a function" from render in every
345
+ // production bundle while dev builds stayed green.
346
+ it('transforms strategy-array patches as production, never the dev JSX runtime', async () => {
347
+ const mockedTransform = vi.mocked(transformSWC)
348
+ mockedTransform.mockClear()
349
+ mockedTransform.mockResolvedValue({ code: 'transformed' } as never)
350
+
351
+ await setupAndPatch({
352
+ patches: [{ module: 'test-pkg', patchFiles: { 'index.js': ['swc'] } }],
353
+ setupFs: async (nm) => {
354
+ const pkgDir = join(nm, 'test-pkg')
355
+ await FSExtra.ensureDir(pkgDir)
356
+ await FSExtra.writeFile(
357
+ join(pkgDir, 'package.json'),
358
+ makePkg('test-pkg', '1.0.0')
359
+ )
360
+ await FSExtra.writeFile(
361
+ join(pkgDir, 'index.js'),
362
+ 'export default () => <div />'
363
+ )
364
+ },
365
+ })
366
+
367
+ expect(mockedTransform).toHaveBeenCalled()
368
+ for (const call of mockedTransform.mock.calls) {
369
+ expect(call[2]).toMatchObject({ production: true })
370
+ }
371
+ })
339
372
  })
@@ -260,8 +260,10 @@ export async function applyDependencyPatches(
260
260
  patchedContent = patchDef
261
261
  } else if (Array.isArray(patchDef)) {
262
262
  // strategy-array patches (flow/jsx/swc transforms) are non-critical
263
- // if they fail (e.g. babel + lru-cache incompatibility on Node 24+),
264
- // warn and skip rather than crashing the whole patch process
263
+ // if they fail, warn and skip rather than crashing the whole patch
264
+ // process. the message carries the real error: a skipped Flow strip
265
+ // here leaves raw Flow in node_modules, and the only sign of it used
266
+ // to be a bundler parse failure several steps later.
265
267
  try {
266
268
  let contents = sourceContent
267
269
  for (const strategy of patchDef) {
@@ -277,6 +279,16 @@ export async function applyDependencyPatches(
277
279
  forceJSX: strategy === 'jsx',
278
280
  noHMR: true,
279
281
  fixNonTypeSpecificImports: true,
282
+ // a patch is written to node_modules ONCE and then read by
283
+ // every later build, dev and production alike, so its output
284
+ // has to be valid in both. the automatic JSX runtime is not:
285
+ // its dev form emits `jsxDEV` from react/jsx-dev-runtime, and
286
+ // React's production build exports `jsxDEV = void 0`, so a
287
+ // patched dependency that renders JSX throws "undefined is not
288
+ // a function" from render in any production bundle. `jsx`/`jsxs`
289
+ // work in both builds, and refresh is already off for
290
+ // node_modules, so the stable runtime is the only correct output.
291
+ production: true,
280
292
  })
281
293
  )?.code || contents
282
294
  }
@@ -288,11 +300,10 @@ export async function applyDependencyPatches(
288
300
  if (!transformWarnedModules.has(patch.module)) {
289
301
  transformWarnedModules.add(patch.module)
290
302
  console.warn(
291
- ` ⚠ Patch transform failed for ${patch.module} (likely Node version compat), skipping non-critical patch`
303
+ ` ⚠ Patch transform failed for ${patch.module} in ${relativePath}, skipping non-critical patch: ${
304
+ (transformErr as Error)?.stack ?? transformErr
305
+ }`
292
306
  )
293
- if (process.env.DEBUG) {
294
- console.warn(transformErr)
295
- }
296
307
  }
297
308
  return
298
309
  }
@@ -55,6 +55,15 @@ export const EXCLUDE_LIST = [
55
55
  '@vxrn/vite-native-hmr',
56
56
  '@vxrn/compiler',
57
57
  '@vxrn/vite-native-client',
58
+
59
+ // build-time only, and they load native .node addons the ssr dep optimizer
60
+ // cannot parse. apps list vxrn as a dependency because it is their dev
61
+ // server, which is exactly why it must never be pre-bundled for ssr.
62
+ 'vxrn',
63
+ '@vxrn/vite-plugin-metro',
64
+ 'oxc-transform',
65
+ 'oxc-parser',
66
+
58
67
  'react-native-ios-utilities',
59
68
  'react-native-ios-modal',
60
69
  'react-native-ios-context-menu',
package/types/index.d.ts CHANGED
@@ -17,5 +17,7 @@ export * from './utils/environmentUtils';
17
17
  export * from './utils/getServerEntry';
18
18
  export * from './plugins/rollupRemoveUnusedImports';
19
19
  export * from './plugins/autoDepOptimizePlugin';
20
+ export * from './plugins/rnCodegenPlugin';
21
+ export * from './plugins/workletImportsPlugin';
20
22
  export * from './types';
21
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAE/D,OAAO,EACL,KAAK,iBAAiB,EACtB,gBAAgB,EAChB,WAAW,GACZ,MAAM,2BAA2B,CAAA;AAClC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,0BAA0B,CAAA;AACxC,cAAc,wBAAwB,CAAA;AAEtC,cAAc,qCAAqC,CAAA;AACnD,cAAc,iCAAiC,CAAA;AAE/C,cAAc,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAE/D,OAAO,EACL,KAAK,iBAAiB,EACtB,gBAAgB,EAChB,WAAW,GACZ,MAAM,2BAA2B,CAAA;AAClC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,0BAA0B,CAAA;AACxC,cAAc,wBAAwB,CAAA;AAEtC,cAAc,qCAAqC,CAAA;AACnD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,2BAA2B,CAAA;AACzC,cAAc,gCAAgC,CAAA;AAE9C,cAAc,SAAS,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"builtInDepPatches.d.ts","sourceRoot":"","sources":["../../src/patches/builtInDepPatches.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAAiC,MAAM,kBAAkB,CAAA;AAE/E,eAAO,MAAM,iBAAiB,EAAE,QAAQ,EAqoBvC,CAAA"}
1
+ {"version":3,"file":"builtInDepPatches.d.ts","sourceRoot":"","sources":["../../src/patches/builtInDepPatches.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAAiC,MAAM,kBAAkB,CAAA;AAE/E,eAAO,MAAM,iBAAiB,EAAE,QAAQ,EA8pBvC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"reactNativeDevServer.d.ts","sourceRoot":"","sources":["../../src/plugins/reactNativeDevServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,MAAM,EAAiB,MAAM,MAAM,CAAA;AAO1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAkBnE,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B9D;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,CAAC,EAAE,OAAO,CACf,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAAC,CACrF,GACA,MAAM,CAmSR"}
1
+ {"version":3,"file":"reactNativeDevServer.d.ts","sourceRoot":"","sources":["../../src/plugins/reactNativeDevServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,MAAM,EAAiB,MAAM,MAAM,CAAA;AAO1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAkBnE,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B9D;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,CAAC,EAAE,OAAO,CACf,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAAC,CACrF,GACA,MAAM,CA+SR"}
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from 'vite';
2
+ export { transformReactNativeCodegen } from '@vxrn/compiler';
3
+ export declare function rnCodegenPlugin(options?: {
4
+ projectRoot?: string;
5
+ }): Plugin;
6
+ //# sourceMappingURL=rnCodegenPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rnCodegenPlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/rnCodegenPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAMlC,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAA;AAE5D,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAW1E"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rnCodegenPlugin.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rnCodegenPlugin.test.d.ts","sourceRoot":"","sources":["../../src/plugins/rnCodegenPlugin.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { type Plugin } from 'rolldown';
2
+ /**
3
+ * bundles selected, synchronous pure-function exports for normal worklets mode.
4
+ * install in native.bundlerOptions.plugins. the replacement module exposes only
5
+ * the configured exports; each entry owns its complete dependency graph.
6
+ */
7
+ export declare function workletImportsPlugin(imports: Record<string, readonly string[]>): Plugin;
8
+ //# sourceMappingURL=workletImportsPlugin.d.ts.map