vxrn 1.12.8 → 1.13.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.
- package/dist/config/getAdditionalViteConfig.mjs +1 -4
- package/dist/config/getAdditionalViteConfig.mjs.map +1 -1
- package/dist/config/getAdditionalViteConfig.native.js +1 -4
- package/dist/config/getAdditionalViteConfig.native.js.map +1 -1
- package/dist/config/getReactNativePlugins.mjs +1 -5
- package/dist/config/getReactNativePlugins.mjs.map +1 -1
- package/dist/config/getReactNativePlugins.native.js +1 -5
- package/dist/config/getReactNativePlugins.native.js.map +1 -1
- package/dist/exports/build.mjs +5 -0
- package/dist/exports/build.mjs.map +1 -1
- package/dist/exports/build.native.js +5 -0
- package/dist/exports/build.native.js.map +1 -1
- package/dist/exports/createServer.mjs +30 -25
- package/dist/exports/createServer.mjs.map +1 -1
- package/dist/exports/createServer.native.js +6 -3
- package/dist/exports/createServer.native.js.map +1 -1
- package/dist/exports/prebuild.mjs +25 -1
- package/dist/exports/prebuild.mjs.map +1 -1
- package/dist/exports/prebuild.native.js +30 -1
- package/dist/exports/prebuild.native.js.map +1 -1
- package/dist/exports/serve.mjs +2 -2
- package/dist/exports/serve.mjs.map +1 -1
- package/dist/exports/serve.native.js +2 -2
- package/dist/exports/serve.native.js.map +1 -1
- package/dist/exports/serveStaticAssets.mjs +43 -11
- package/dist/exports/serveStaticAssets.mjs.map +1 -1
- package/dist/exports/serveStaticAssets.native.js +69 -6
- package/dist/exports/serveStaticAssets.native.js.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -2
- package/dist/index.mjs.map +1 -1
- package/dist/index.native.js +1 -2
- package/dist/index.native.js.map +1 -1
- package/dist/patches/builtInDepPatches.mjs +14 -0
- package/dist/patches/builtInDepPatches.mjs.map +1 -1
- package/dist/patches/builtInDepPatches.native.js +14 -0
- package/dist/patches/builtInDepPatches.native.js.map +1 -1
- package/dist/plugins/clientInjectPlugin.mjs +2 -62
- package/dist/plugins/clientInjectPlugin.mjs.map +1 -1
- package/dist/plugins/clientInjectPlugin.native.js +2 -75
- package/dist/plugins/clientInjectPlugin.native.js.map +1 -1
- package/dist/plugins/reactNativeDevAssetPlugin.mjs +1 -3
- package/dist/plugins/reactNativeDevAssetPlugin.mjs.map +1 -1
- package/dist/plugins/reactNativeDevAssetPlugin.native.js +1 -3
- package/dist/plugins/reactNativeDevAssetPlugin.native.js.map +1 -1
- package/dist/plugins/reactNativeDevServer.mjs +55 -77
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +73 -84
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/plugins/serverExtensions.test.mjs +12 -6
- package/dist/plugins/serverExtensions.test.mjs.map +1 -1
- package/dist/plugins/serverExtensions.test.native.js +12 -6
- package/dist/plugins/serverExtensions.test.native.js.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.mjs +11 -35
- package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.native.js +9 -36
- package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
- package/dist/runtime/hmr-client.mjs +125 -0
- package/dist/runtime/hmr-client.mjs.map +1 -0
- package/dist/runtime/hmr-client.native.js +197 -0
- package/dist/runtime/hmr-client.native.js.map +1 -0
- package/dist/runtime/hmr-runtime.mjs +162 -0
- package/dist/runtime/hmr-runtime.mjs.map +1 -0
- package/dist/runtime/hmr-runtime.native.js +348 -0
- package/dist/runtime/hmr-runtime.native.js.map +1 -0
- package/dist/runtime/hmr-types.mjs +2 -0
- package/dist/runtime/hmr-types.mjs.map +1 -0
- package/dist/runtime/hmr-types.native.js +2 -0
- package/dist/runtime/hmr-types.native.js.map +1 -0
- package/dist/runtime/native-prelude.mjs +97 -0
- package/dist/runtime/native-prelude.mjs.map +1 -0
- package/dist/runtime/native-prelude.native.js +97 -0
- package/dist/runtime/native-prelude.native.js.map +1 -0
- package/dist/runtime/react-refresh-utils.mjs +19 -0
- package/dist/runtime/react-refresh-utils.mjs.map +1 -0
- package/dist/runtime/react-refresh-utils.native.js +24 -0
- package/dist/runtime/react-refresh-utils.native.js.map +1 -0
- package/dist/utils/createNativeDevEngine.mjs +661 -0
- package/dist/utils/createNativeDevEngine.mjs.map +1 -0
- package/dist/utils/createNativeDevEngine.native.js +702 -0
- package/dist/utils/createNativeDevEngine.native.js.map +1 -0
- package/dist/utils/patches.mjs +6 -2
- package/dist/utils/patches.mjs.map +1 -1
- package/dist/utils/patches.native.js +6 -2
- package/dist/utils/patches.native.js.map +1 -1
- package/dist/utils/scanDepsToOptimize.mjs +4 -3
- package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
- package/dist/utils/scanDepsToOptimize.native.js +4 -3
- package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
- package/expo-plugin.cjs +122 -0
- package/package.json +15 -19
- package/src/config/getAdditionalViteConfig.ts +1 -3
- package/src/config/getReactNativePlugins.ts +0 -6
- package/src/exports/build.ts +5 -0
- package/src/exports/createServer.ts +7 -2
- package/src/exports/prebuild.ts +45 -0
- package/src/exports/serve.ts +2 -1
- package/src/exports/serveStaticAssets.ts +67 -4
- package/src/index.ts +0 -2
- package/src/patches/builtInDepPatches.ts +29 -0
- package/src/plugins/clientInjectPlugin.ts +2 -109
- package/src/plugins/reactNativeDevAssetPlugin.ts +0 -21
- package/src/plugins/reactNativeDevServer.ts +57 -84
- package/src/plugins/serverExtensions.test.ts +6 -8
- package/src/rn-commands/bundle/buildBundle.ts +9 -62
- package/src/runtime/hmr-client.ts +215 -0
- package/src/runtime/hmr-runtime.ts +276 -0
- package/src/runtime/hmr-types.ts +84 -0
- package/src/runtime/native-prelude.ts +110 -0
- package/src/runtime/react-refresh-utils.ts +36 -0
- package/src/types.ts +22 -4
- package/src/utils/createNativeDevEngine.ts +942 -0
- package/src/utils/patches.ts +36 -18
- package/src/utils/scanDepsToOptimize.ts +2 -3
- package/types/config/getAdditionalViteConfig.d.ts.map +1 -1
- package/types/config/getOptionsFilled.d.ts +2 -18
- package/types/config/getOptionsFilled.d.ts.map +1 -1
- package/types/config/getReactNativePlugins.d.ts.map +1 -1
- package/types/exports/build.d.ts +1 -9
- package/types/exports/build.d.ts.map +1 -1
- package/types/exports/createServer.d.ts.map +1 -1
- package/types/exports/prebuild.d.ts.map +1 -1
- package/types/exports/serve.d.ts +2 -1
- package/types/exports/serve.d.ts.map +1 -1
- package/types/exports/serveStaticAssets.d.ts +12 -1
- package/types/exports/serveStaticAssets.d.ts.map +1 -1
- package/types/index.d.ts +0 -1
- package/types/index.d.ts.map +1 -1
- package/types/patches/builtInDepPatches.d.ts.map +1 -1
- package/types/plugins/clientInjectPlugin.d.ts +1 -7
- package/types/plugins/clientInjectPlugin.d.ts.map +1 -1
- package/types/plugins/reactNativeDevAssetPlugin.d.ts.map +1 -1
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
- package/types/runtime/hmr-client.d.ts +40 -0
- package/types/runtime/hmr-client.d.ts.map +1 -0
- package/types/runtime/hmr-runtime.d.ts +69 -0
- package/types/runtime/hmr-runtime.d.ts.map +1 -0
- package/types/runtime/hmr-types.d.ts +76 -0
- package/types/runtime/hmr-types.d.ts.map +1 -0
- package/types/runtime/native-prelude.d.ts +11 -0
- package/types/runtime/native-prelude.d.ts.map +1 -0
- package/types/runtime/react-refresh-utils.d.ts +3 -0
- package/types/runtime/react-refresh-utils.d.ts.map +1 -0
- package/types/types.d.ts +15 -1
- package/types/types.d.ts.map +1 -1
- package/types/utils/createNativeDevEngine.d.ts +42 -0
- package/types/utils/createNativeDevEngine.d.ts.map +1 -0
- package/types/utils/patches.d.ts.map +1 -1
- package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
- package/dist/config/getReactNativeBuildConfig.mjs +0 -200
- package/dist/config/getReactNativeBuildConfig.mjs.map +0 -1
- package/dist/config/getReactNativeBuildConfig.native.js +0 -204
- package/dist/config/getReactNativeBuildConfig.native.js.map +0 -1
- package/dist/plugins/reactNativeHMRPlugin.mjs +0 -120
- package/dist/plugins/reactNativeHMRPlugin.mjs.map +0 -1
- package/dist/plugins/reactNativeHMRPlugin.native.js +0 -151
- package/dist/plugins/reactNativeHMRPlugin.native.js.map +0 -1
- package/dist/utils/filterPluginsForNative.mjs +0 -27
- package/dist/utils/filterPluginsForNative.mjs.map +0 -1
- package/dist/utils/filterPluginsForNative.native.js +0 -33
- package/dist/utils/filterPluginsForNative.native.js.map +0 -1
- package/dist/utils/getReactNativeBundle.mjs +0 -104
- package/dist/utils/getReactNativeBundle.mjs.map +0 -1
- package/dist/utils/getReactNativeBundle.native.js +0 -135
- package/dist/utils/getReactNativeBundle.native.js.map +0 -1
- package/dist/utils/hotUpdateCache.mjs +0 -3
- package/dist/utils/hotUpdateCache.mjs.map +0 -1
- package/dist/utils/hotUpdateCache.native.js +0 -3
- package/dist/utils/hotUpdateCache.native.js.map +0 -1
- package/dist/utils/isBuildingNativeBundle.mjs +0 -6
- package/dist/utils/isBuildingNativeBundle.mjs.map +0 -1
- package/dist/utils/isBuildingNativeBundle.native.js +0 -7
- package/dist/utils/isBuildingNativeBundle.native.js.map +0 -1
- package/dist/utils/swapPrebuiltReactModules.mjs +0 -168
- package/dist/utils/swapPrebuiltReactModules.mjs.map +0 -1
- package/dist/utils/swapPrebuiltReactModules.native.js +0 -181
- package/dist/utils/swapPrebuiltReactModules.native.js.map +0 -1
- package/dist/worker.mjs +0 -55
- package/dist/worker.mjs.map +0 -1
- package/dist/worker.native.js +0 -55
- package/dist/worker.native.js.map +0 -1
- package/react-native-template.js +0 -375
- package/src/config/getReactNativeBuildConfig.ts +0 -349
- package/src/plugins/reactNativeHMRPlugin.ts +0 -237
- package/src/utils/filterPluginsForNative.ts +0 -55
- package/src/utils/getReactNativeBundle.ts +0 -243
- package/src/utils/hotUpdateCache.ts +0 -1
- package/src/utils/isBuildingNativeBundle.ts +0 -7
- package/src/utils/swapPrebuiltReactModules.ts +0 -341
- package/src/worker.ts +0 -90
- package/types/config/getReactNativeBuildConfig.d.ts +0 -72
- package/types/config/getReactNativeBuildConfig.d.ts.map +0 -1
- package/types/plugins/reactNativeHMRPlugin.d.ts +0 -10
- package/types/plugins/reactNativeHMRPlugin.d.ts.map +0 -1
- package/types/utils/filterPluginsForNative.d.ts +0 -8
- package/types/utils/filterPluginsForNative.d.ts.map +0 -1
- package/types/utils/getReactNativeBundle.d.ts +0 -12
- package/types/utils/getReactNativeBundle.d.ts.map +0 -1
- package/types/utils/hotUpdateCache.d.ts +0 -2
- package/types/utils/hotUpdateCache.d.ts.map +0 -1
- package/types/utils/isBuildingNativeBundle.d.ts +0 -3
- package/types/utils/isBuildingNativeBundle.d.ts.map +0 -1
- package/types/utils/swapPrebuiltReactModules.d.ts +0 -9
- package/types/utils/swapPrebuiltReactModules.d.ts.map +0 -1
- package/types/worker.d.ts +0 -13
- package/types/worker.d.ts.map +0 -1
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a rolldown DevEngine for native React Native bundle serving.
|
|
3
|
+
* Uses rolldown's experimental dev() API with ESM output.
|
|
4
|
+
*
|
|
5
|
+
* Inspired by rollipop's architecture:
|
|
6
|
+
* https://github.com/leegeunhyeok/rollipop
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { writeFileSync } from 'node:fs'
|
|
10
|
+
import { basename, dirname, extname, join, relative } from 'node:path'
|
|
11
|
+
import type { InputOptions, OutputOptions, Plugin, RolldownOutput } from 'rolldown'
|
|
12
|
+
import { DEFAULT_ASSET_EXTS } from '../constants/defaults'
|
|
13
|
+
import { getNativePrelude } from '../runtime/native-prelude'
|
|
14
|
+
|
|
15
|
+
// files that contain Flow syntax and need stripping
|
|
16
|
+
const FLOW_FILE_PATTERN = /node_modules[\\/](?:react-native|@react-native)[\\/].*\.js$/
|
|
17
|
+
|
|
18
|
+
interface NativeDevEngineOptions {
|
|
19
|
+
root: string
|
|
20
|
+
port: number
|
|
21
|
+
host?: string
|
|
22
|
+
platform: 'ios' | 'android'
|
|
23
|
+
serverUrl?: string
|
|
24
|
+
plugins?: Plugin[]
|
|
25
|
+
onHmrUpdate?: (update: { type: string; code?: string }) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface NativeDevEngineResult {
|
|
29
|
+
engine: any
|
|
30
|
+
getBundle: () => Promise<{ code: string; map?: string }>
|
|
31
|
+
close: () => Promise<void>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// shared resolve extensions for native builds
|
|
35
|
+
function getResolveExtensions(platform: 'ios' | 'android'): string[] {
|
|
36
|
+
const platformExts =
|
|
37
|
+
platform === 'ios'
|
|
38
|
+
? ['.ios.tsx', '.ios.ts', '.ios.jsx', '.ios.js']
|
|
39
|
+
: ['.android.tsx', '.android.ts', '.android.jsx', '.android.js']
|
|
40
|
+
const nativeExts = ['.native.tsx', '.native.ts', '.native.jsx', '.native.js']
|
|
41
|
+
const defaultExts = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json']
|
|
42
|
+
return [...platformExts, ...nativeExts, ...defaultExts]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// shared rolldown resolve config for native builds
|
|
46
|
+
function getNativeResolveConfig(platform: 'ios' | 'android') {
|
|
47
|
+
return {
|
|
48
|
+
extensions: getResolveExtensions(platform),
|
|
49
|
+
conditionNames: ['react-native', 'import', 'require', 'default'],
|
|
50
|
+
mainFields: ['react-native', 'module', 'main'],
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// shared rolldown transform config for native builds
|
|
55
|
+
function getNativeTransformConfig(dev: boolean) {
|
|
56
|
+
return {
|
|
57
|
+
jsx: {
|
|
58
|
+
// use 'classic' mode (babel plugin-transform-react-jsx)
|
|
59
|
+
// 'automatic' has files where jsxDEV import fails to resolve
|
|
60
|
+
runtime: 'classic' as const,
|
|
61
|
+
},
|
|
62
|
+
define: {
|
|
63
|
+
'process.env.NODE_ENV': dev ? '"development"' : '"production"',
|
|
64
|
+
'process.env.VXRN_REACT_19': 'false',
|
|
65
|
+
__DEV__: dev ? 'true' : 'false',
|
|
66
|
+
},
|
|
67
|
+
// auto-inject React import for classic JSX (React.createElement)
|
|
68
|
+
inject: {
|
|
69
|
+
React: 'react',
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// shared plugins used by both dev and prod native builds
|
|
75
|
+
function getNativePlugins(
|
|
76
|
+
root: string,
|
|
77
|
+
platform: string,
|
|
78
|
+
viteImportGlobPlugin: any,
|
|
79
|
+
dev: boolean
|
|
80
|
+
): Plugin[] {
|
|
81
|
+
return [
|
|
82
|
+
// stub CSS imports — native doesn't support CSS and rolldown removed CSS bundling
|
|
83
|
+
cssStubPlugin(),
|
|
84
|
+
// handle import.meta.glob (used by One's route system)
|
|
85
|
+
viteImportGlobPlugin({ root }) as any,
|
|
86
|
+
// strip Flow types from react-native and @react-native packages
|
|
87
|
+
flowStripPlugin(),
|
|
88
|
+
// handle asset imports (.png, .jpg, .ttf, etc.)
|
|
89
|
+
assetPlugin({ root, platform }),
|
|
90
|
+
// @vxrn/compiler babel transforms: reanimated worklets, async generators,
|
|
91
|
+
// react-native codegen, react compiler — same pipeline as metro
|
|
92
|
+
vxrnCompilerPlugin(platform, dev),
|
|
93
|
+
// hermes compat: transform class properties and private fields
|
|
94
|
+
hermesCompatSWCPlugin(dev),
|
|
95
|
+
// downgrade polyfill "not configurable" errors to warnings (hermes v1)
|
|
96
|
+
polyfillErrorDowngradePlugin(),
|
|
97
|
+
// strip DevSettings in prod (dev-only native module)
|
|
98
|
+
stripDevSettingsPlugin(dev),
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// shared output options for native builds
|
|
103
|
+
function getNativeOutputOptions(prelude: string): OutputOptions {
|
|
104
|
+
return {
|
|
105
|
+
format: 'esm',
|
|
106
|
+
sourcemap: true,
|
|
107
|
+
intro: prelude,
|
|
108
|
+
codeSplitting: false,
|
|
109
|
+
strictExecutionOrder: true,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Post-process a native bundle to fix rolldown devMode output quirks.
|
|
115
|
+
* Most concerns have been moved to plugins/config:
|
|
116
|
+
* - VXRN_REACT_19 → handled by define in getNativeTransformConfig
|
|
117
|
+
* - polyfill error downgrade → polyfillErrorDowngradePlugin
|
|
118
|
+
* - DevSettings stripping → stripDevSettingsPlugin
|
|
119
|
+
*/
|
|
120
|
+
function postProcessNativeBundle(code: string): string {
|
|
121
|
+
// rolldown devMode still emits ESM export statements that hermes can't parse.
|
|
122
|
+
// this is a rolldown behavior we can't configure away yet.
|
|
123
|
+
code = code.replace(/^\s*export\s*\{[^}]*\}\s*;?\s*$/gm, '')
|
|
124
|
+
// rolldown devMode runtime leaves some raw import.meta.hot references
|
|
125
|
+
// that aren't compiled through the normal plugin pipeline.
|
|
126
|
+
code = code.replace(/^if \(import\.meta\.hot\).*$/gm, '')
|
|
127
|
+
return code
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Downlevel class fields in the rolldown runtime for Hermes compatibility.
|
|
132
|
+
* The runtime (\0rolldown/runtime.js) is injected directly into the output,
|
|
133
|
+
* bypassing hermesCompatSWCPlugin. We extract just that section (~5KB) and
|
|
134
|
+
* transform it rather than re-parsing the entire 6MB bundle.
|
|
135
|
+
*/
|
|
136
|
+
async function downlevelClassFieldsInBundle(code: string): Promise<string> {
|
|
137
|
+
const startMarker = '//#region \\0rolldown/runtime.js'
|
|
138
|
+
const endMarker = '//#endregion'
|
|
139
|
+
|
|
140
|
+
const startIdx = code.indexOf(startMarker)
|
|
141
|
+
if (startIdx === -1) return code
|
|
142
|
+
|
|
143
|
+
const endIdx = code.indexOf(endMarker, startIdx)
|
|
144
|
+
if (endIdx === -1) return code
|
|
145
|
+
|
|
146
|
+
const runtimeEnd = endIdx + endMarker.length
|
|
147
|
+
const runtimeSection = code.slice(startIdx, runtimeEnd)
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const swc = await import('@swc/core')
|
|
151
|
+
const result = await swc.transform(runtimeSection, {
|
|
152
|
+
filename: 'rolldown-runtime.js',
|
|
153
|
+
configFile: false,
|
|
154
|
+
swcrc: false,
|
|
155
|
+
sourceMaps: false,
|
|
156
|
+
inputSourceMap: false,
|
|
157
|
+
isModule: false,
|
|
158
|
+
env: {
|
|
159
|
+
targets: { node: 9999 },
|
|
160
|
+
include: [
|
|
161
|
+
'transform-class-properties',
|
|
162
|
+
'transform-class-static-block',
|
|
163
|
+
'transform-private-methods',
|
|
164
|
+
'transform-private-property-in-object',
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
jsc: {
|
|
168
|
+
parser: { syntax: 'ecmascript' },
|
|
169
|
+
transform: { react: { runtime: 'preserve' } },
|
|
170
|
+
externalHelpers: false,
|
|
171
|
+
assumptions: {
|
|
172
|
+
setPublicClassFields: true,
|
|
173
|
+
privateFieldsAsProperties: true,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
return code.slice(0, startIdx) + result.code + code.slice(runtimeEnd)
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn('[vxrn] downlevelClassFieldsInBundle failed, returning original:', err)
|
|
180
|
+
return code
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function createNativeDevEngine(
|
|
185
|
+
options: NativeDevEngineOptions
|
|
186
|
+
): Promise<NativeDevEngineResult> {
|
|
187
|
+
const {
|
|
188
|
+
root,
|
|
189
|
+
port,
|
|
190
|
+
host = 'localhost',
|
|
191
|
+
platform,
|
|
192
|
+
serverUrl,
|
|
193
|
+
plugins: userPlugins = [],
|
|
194
|
+
onHmrUpdate,
|
|
195
|
+
} = options
|
|
196
|
+
|
|
197
|
+
const { dev, viteImportGlobPlugin } = await import('rolldown/experimental')
|
|
198
|
+
|
|
199
|
+
const hmrRuntimeSource = getHmrRuntimeSource()
|
|
200
|
+
|
|
201
|
+
const prelude = getNativePrelude({
|
|
202
|
+
dev: true,
|
|
203
|
+
platform,
|
|
204
|
+
serverUrl: serverUrl || `http://${host}:${port}`,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// generate a real entry file on disk (virtual modules can't use import.meta.glob)
|
|
208
|
+
const entryFile = generateNativeEntry(root)
|
|
209
|
+
|
|
210
|
+
let currentBundle: { code: string; map?: string } | null = null
|
|
211
|
+
let bundleResolve: ((value: any) => void) | null = null
|
|
212
|
+
let bundlePromise: Promise<any> | null = null
|
|
213
|
+
|
|
214
|
+
const resolvedHost = host === '0.0.0.0' ? 'localhost' : host
|
|
215
|
+
|
|
216
|
+
const inputOptions: InputOptions = {
|
|
217
|
+
input: entryFile,
|
|
218
|
+
cwd: root,
|
|
219
|
+
platform: 'neutral',
|
|
220
|
+
resolve: getNativeResolveConfig(platform),
|
|
221
|
+
transform: getNativeTransformConfig(true),
|
|
222
|
+
|
|
223
|
+
experimental: {
|
|
224
|
+
devMode: { implement: hmrRuntimeSource, host, port },
|
|
225
|
+
incrementalBuild: true,
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
treeshake: false,
|
|
229
|
+
// some react-native ecosystem packages import symbols that don't exist in
|
|
230
|
+
// the declared entry (e.g. @react-navigation/elements imports NavigationProvider
|
|
231
|
+
// from @react-navigation/native which doesn't export it). metro silently shims
|
|
232
|
+
// these — rolldown needs an explicit opt-in.
|
|
233
|
+
shimMissingExports: true,
|
|
234
|
+
|
|
235
|
+
moduleTypes: {
|
|
236
|
+
'.js': 'jsx',
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
plugins: [
|
|
240
|
+
...getNativePlugins(root, platform, viteImportGlobPlugin, true),
|
|
241
|
+
|
|
242
|
+
// add import.meta.hot.accept() to user files for HMR boundaries
|
|
243
|
+
// rolldown compiles import.meta.hot -> createModuleHotContext at build time
|
|
244
|
+
nativeReactRefreshPlugin(),
|
|
245
|
+
|
|
246
|
+
...userPlugins,
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const outputOptions: OutputOptions = {
|
|
251
|
+
...getNativeOutputOptions(prelude),
|
|
252
|
+
// connect HMR WebSocket using RN's WebSocket module (not the global)
|
|
253
|
+
outro: `
|
|
254
|
+
try {
|
|
255
|
+
var __WS = (init_WebSocket(), __toCommonJS(WebSocket_exports)).default;
|
|
256
|
+
var __hmrUrl = 'ws://${resolvedHost}:${port}/hot';
|
|
257
|
+
var __hmrWS = new __WS(__hmrUrl);
|
|
258
|
+
__hmrWS.onmessage = function(event) {
|
|
259
|
+
try {
|
|
260
|
+
var msg = JSON.parse(event.data);
|
|
261
|
+
var g = typeof global !== 'undefined' ? global : globalThis;
|
|
262
|
+
if (msg.type === 'hmr:update' && msg.code) {
|
|
263
|
+
if (g.globalEvalWithSourceUrl) g.globalEvalWithSourceUrl(msg.code);
|
|
264
|
+
else (0, eval)(msg.code);
|
|
265
|
+
setTimeout(function() {
|
|
266
|
+
try { if (g.__ReactRefresh) g.__ReactRefresh.performReactRefresh(); } catch(e) {}
|
|
267
|
+
}, 50);
|
|
268
|
+
} else if (msg.type === 'hmr:reload') {
|
|
269
|
+
var ds = g.__turboModuleProxy ? g.__turboModuleProxy('DevSettings') : null;
|
|
270
|
+
if (ds && ds.reload) ds.reload();
|
|
271
|
+
}
|
|
272
|
+
} catch(e) { console.error('[vxrn] HMR eval error:', e); }
|
|
273
|
+
};
|
|
274
|
+
__hmrWS.onopen = function() {
|
|
275
|
+
if (typeof __rolldown_runtime__ !== 'undefined' && __rolldown_runtime__.setup) {
|
|
276
|
+
__rolldown_runtime__.setup(__hmrWS, __hmrUrl.replace('ws://', 'http://'));
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
__hmrWS.onerror = function(e) { console.warn('[vxrn] HMR connection error:', e.message || e); };
|
|
280
|
+
} catch(e) {}
|
|
281
|
+
`,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const engine = await dev(inputOptions, outputOptions, {
|
|
285
|
+
onOutput: async (result) => {
|
|
286
|
+
if (result instanceof Error) {
|
|
287
|
+
console.error('[vxrn] native bundle error:', result.message)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const output = result as RolldownOutput
|
|
292
|
+
const chunk = output.output.find((o) => o.type === 'chunk' && o.isEntry)
|
|
293
|
+
if (chunk && 'code' in chunk) {
|
|
294
|
+
let code = postProcessNativeBundle(chunk.code)
|
|
295
|
+
|
|
296
|
+
// downlevel class fields from the rolldown runtime (virtual module
|
|
297
|
+
// skipped by the per-file SWC plugin) so old Hermes can parse them
|
|
298
|
+
code = await downlevelClassFieldsInBundle(code)
|
|
299
|
+
|
|
300
|
+
// register a no-op HMRClient so RN's native side doesn't error when calling HMRClient.setup()
|
|
301
|
+
// our actual HMR is handled via the outro WebSocket connection
|
|
302
|
+
const hmrClientStub = `registerCallableModule("HMRClient",{setup:function(){},enable:function(){},disable:function(){},registerBundle:function(){},log:function(){}})`
|
|
303
|
+
code = code.replace(
|
|
304
|
+
/registerCallableModule\s*\(\s*["']AppRegistry["']/,
|
|
305
|
+
(match) => hmrClientStub + ',' + match
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
currentBundle = {
|
|
309
|
+
code,
|
|
310
|
+
map: chunk.map?.toString(),
|
|
311
|
+
}
|
|
312
|
+
console.info(
|
|
313
|
+
`[vxrn] native bundle ready (${Math.round(chunk.code.length / 1024)}KB)`
|
|
314
|
+
)
|
|
315
|
+
if (bundleResolve) {
|
|
316
|
+
bundleResolve(currentBundle)
|
|
317
|
+
bundleResolve = null
|
|
318
|
+
bundlePromise = null
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
onHmrUpdates: async (result) => {
|
|
324
|
+
if (result instanceof Error) {
|
|
325
|
+
console.error('[vxrn] HMR error:', result.message)
|
|
326
|
+
onHmrUpdate?.({ type: 'hmr:error' })
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
const updates = (result as any).updates || []
|
|
330
|
+
|
|
331
|
+
for (const item of updates) {
|
|
332
|
+
const update = item.update || item
|
|
333
|
+
if (update.type === 'Patch' && update.code) {
|
|
334
|
+
onHmrUpdate?.({ type: 'hmr:update', code: update.code })
|
|
335
|
+
} else if (update.type === 'FullReload') {
|
|
336
|
+
onHmrUpdate?.({ type: 'hmr:reload' })
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (updates.length === 0) {
|
|
341
|
+
onHmrUpdate?.({ type: 'hmr:reload' })
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
rebuildStrategy: 'auto',
|
|
346
|
+
watch: {},
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
await engine.run()
|
|
350
|
+
|
|
351
|
+
// modules are registered via WebSocket messages from the HMR client
|
|
352
|
+
// (the devMode runtime sends hmr:module-registered messages)
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
engine,
|
|
356
|
+
|
|
357
|
+
async getBundle() {
|
|
358
|
+
if (currentBundle) return currentBundle
|
|
359
|
+
if (!bundlePromise) {
|
|
360
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
361
|
+
bundlePromise = new Promise((resolve, reject) => {
|
|
362
|
+
bundleResolve = (value) => {
|
|
363
|
+
clearTimeout(timeoutId)
|
|
364
|
+
resolve(value)
|
|
365
|
+
}
|
|
366
|
+
timeoutId = setTimeout(
|
|
367
|
+
() => reject(new Error('[vxrn] bundle build timed out after 120s')),
|
|
368
|
+
120_000
|
|
369
|
+
)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
await engine.ensureLatestBuildOutput()
|
|
373
|
+
if (currentBundle) return currentBundle
|
|
374
|
+
return bundlePromise
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
async close() {
|
|
378
|
+
await engine.close()
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- production build ---
|
|
384
|
+
|
|
385
|
+
interface NativeBuildOptions {
|
|
386
|
+
root: string
|
|
387
|
+
platform: 'ios' | 'android'
|
|
388
|
+
dev?: boolean
|
|
389
|
+
serverUrl?: string
|
|
390
|
+
plugins?: Plugin[]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function buildNativeBundle(
|
|
394
|
+
options: NativeBuildOptions
|
|
395
|
+
): Promise<{ code: string; map?: string }> {
|
|
396
|
+
const { root, platform, dev = false, serverUrl, plugins: userPlugins = [] } = options
|
|
397
|
+
|
|
398
|
+
const { build } = await import('rolldown')
|
|
399
|
+
const { viteImportGlobPlugin } = await import('rolldown/experimental')
|
|
400
|
+
|
|
401
|
+
const prelude = getNativePrelude({
|
|
402
|
+
dev,
|
|
403
|
+
platform,
|
|
404
|
+
serverUrl,
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
const entryFile = generateNativeEntry(root, { dev })
|
|
408
|
+
|
|
409
|
+
const result = await build({
|
|
410
|
+
input: entryFile,
|
|
411
|
+
cwd: root,
|
|
412
|
+
platform: 'neutral',
|
|
413
|
+
resolve: getNativeResolveConfig(platform),
|
|
414
|
+
transform: getNativeTransformConfig(dev),
|
|
415
|
+
treeshake: !dev,
|
|
416
|
+
shimMissingExports: true,
|
|
417
|
+
moduleTypes: { '.js': 'jsx' },
|
|
418
|
+
plugins: [
|
|
419
|
+
...getNativePlugins(root, platform, viteImportGlobPlugin, dev),
|
|
420
|
+
...userPlugins,
|
|
421
|
+
],
|
|
422
|
+
output: getNativeOutputOptions(prelude),
|
|
423
|
+
})
|
|
424
|
+
const chunk = result.output.find((o) => o.type === 'chunk' && o.isEntry)
|
|
425
|
+
|
|
426
|
+
if (!chunk || !('code' in chunk)) {
|
|
427
|
+
throw new Error('[vxrn] production build produced no output')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let code = postProcessNativeBundle(chunk.code)
|
|
431
|
+
code = await downlevelClassFieldsInBundle(code)
|
|
432
|
+
return { code, map: chunk.map?.toString() }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function generateNativeEntry(root: string, opts?: { dev?: boolean }): string {
|
|
436
|
+
const isDev = opts?.dev !== false
|
|
437
|
+
// write entry at project root so import.meta.glob('./app/...') resolves correctly
|
|
438
|
+
const entryPath = join(root, '.vxrn-entry-native.tsx')
|
|
439
|
+
|
|
440
|
+
const refreshSetup = isDev
|
|
441
|
+
? `
|
|
442
|
+
// react-refresh/runtime MUST initialize before React loads
|
|
443
|
+
import RefreshRuntime from 'react-refresh/runtime';
|
|
444
|
+
RefreshRuntime.injectIntoGlobalHook(globalThis);
|
|
445
|
+
globalThis.__ReactRefresh = RefreshRuntime;
|
|
446
|
+
globalThis.$RefreshReg$ = function(type, id) {
|
|
447
|
+
RefreshRuntime.register(type, id);
|
|
448
|
+
};
|
|
449
|
+
globalThis.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
|
|
450
|
+
`
|
|
451
|
+
: ''
|
|
452
|
+
|
|
453
|
+
const entryCode = `
|
|
454
|
+
// auto-generated native entry for rolldown
|
|
455
|
+
${refreshSetup}
|
|
456
|
+
import { createApp } from 'one';
|
|
457
|
+
|
|
458
|
+
var _routes = import.meta.glob(['./app/**/*.tsx', './app/**/*.ts', '!./app/**/*+api.*', '!./app/**/*.test.*', '!./app/**/*.d.ts'], { exhaustive: true });
|
|
459
|
+
// fix route keys: One expects '/app/...' prefix but import.meta.glob returns './app/...'
|
|
460
|
+
var routes = {};
|
|
461
|
+
Object.keys(_routes).forEach(function(key) {
|
|
462
|
+
routes[key.replace(/^\\./, '')] = _routes[key];
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
createApp({
|
|
466
|
+
routes: routes,
|
|
467
|
+
routerRoot: 'app',
|
|
468
|
+
flags: {},
|
|
469
|
+
});
|
|
470
|
+
`
|
|
471
|
+
writeFileSync(entryPath, entryCode)
|
|
472
|
+
return entryPath
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// --- plugins ---
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Stub CSS imports for native builds.
|
|
479
|
+
* Native doesn't support CSS and rolldown removed CSS bundling support.
|
|
480
|
+
* Without this, any `import './foo.css'` will cause a build error.
|
|
481
|
+
*/
|
|
482
|
+
function cssStubPlugin(): Plugin {
|
|
483
|
+
return {
|
|
484
|
+
name: 'vxrn:css-stub',
|
|
485
|
+
load: {
|
|
486
|
+
handler(id) {
|
|
487
|
+
if (/\.css$/.test(id)) {
|
|
488
|
+
return { code: '', moduleType: 'js' as any }
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Pipe files through @vxrn/compiler's babel transforms.
|
|
497
|
+
* Handles reanimated worklet compilation, async generator downleveling,
|
|
498
|
+
* react-native codegen, and react compiler — same pipeline as metro.
|
|
499
|
+
* Auto-detects which transforms are needed per file.
|
|
500
|
+
*/
|
|
501
|
+
function vxrnCompilerPlugin(platform: string, dev: boolean): Plugin {
|
|
502
|
+
let compiler: typeof import('@vxrn/compiler') | null = null
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
name: 'vxrn:compiler',
|
|
506
|
+
async transform(code, id) {
|
|
507
|
+
if (!/\.[cm]?[jt]sx?$/.test(id)) return
|
|
508
|
+
if (id.includes('\0') || id.includes('virtual:')) return
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
if (!compiler) compiler = await import('@vxrn/compiler')
|
|
512
|
+
|
|
513
|
+
const props = {
|
|
514
|
+
id,
|
|
515
|
+
code,
|
|
516
|
+
development: dev,
|
|
517
|
+
environment: platform as 'ios' | 'android',
|
|
518
|
+
reactForRNVersion: '19' as const,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const babelOptions = compiler.getBabelOptions(props)
|
|
522
|
+
if (!babelOptions) return
|
|
523
|
+
|
|
524
|
+
const result = await compiler.transformBabel(id, code, babelOptions)
|
|
525
|
+
|
|
526
|
+
if (result?.code) {
|
|
527
|
+
return { code: result.code }
|
|
528
|
+
}
|
|
529
|
+
} catch (err: any) {
|
|
530
|
+
// log but don't crash — fallback to rolldown's own transform
|
|
531
|
+
if (dev) {
|
|
532
|
+
console.warn(`[vxrn:compiler] ${id}: ${err.message || err}`)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Strip Flow types from react-native source files.
|
|
541
|
+
* Uses hermes-parser which is already a dep of react-native.
|
|
542
|
+
*/
|
|
543
|
+
function flowStripPlugin(): Plugin {
|
|
544
|
+
return {
|
|
545
|
+
name: 'vxrn:flow-strip',
|
|
546
|
+
transform: {
|
|
547
|
+
async handler(code, id) {
|
|
548
|
+
if (!FLOW_FILE_PATTERN.test(id)) return
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const fft = await import('fast-flow-transform')
|
|
552
|
+
const result = await fft.default({
|
|
553
|
+
filename: id,
|
|
554
|
+
source: code,
|
|
555
|
+
sourcemap: true,
|
|
556
|
+
dialect: 'flow',
|
|
557
|
+
format: 'pretty',
|
|
558
|
+
})
|
|
559
|
+
// don't set moduleType - let rolldown's global moduleTypes config handle it
|
|
560
|
+
return { code: result.code, map: result.map }
|
|
561
|
+
} catch (err: any) {
|
|
562
|
+
console.warn(`[vxrn:flow-strip] ${id}: ${err.message}`)
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Handle asset imports (.png, .jpg, .ttf, etc.)
|
|
571
|
+
* Returns JS code that registers the asset with RN's AssetRegistry.
|
|
572
|
+
*/
|
|
573
|
+
function assetPlugin(opts: { root: string; platform: string }): Plugin {
|
|
574
|
+
const assetRegex = new RegExp(`\\.(?:${DEFAULT_ASSET_EXTS.join('|')})$`)
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
name: 'vxrn:asset',
|
|
578
|
+
load: {
|
|
579
|
+
async handler(id) {
|
|
580
|
+
if (!assetRegex.test(id)) return
|
|
581
|
+
|
|
582
|
+
const ext = extname(id).slice(1)
|
|
583
|
+
const name = basename(id, `.${ext}`)
|
|
584
|
+
const dir = dirname(id)
|
|
585
|
+
const relativePath = relative(opts.root, id)
|
|
586
|
+
const httpLocation = '/assets/' + dirname(relativePath)
|
|
587
|
+
|
|
588
|
+
// simple asset registration (TODO: scale detection like rollipop)
|
|
589
|
+
const assetData = {
|
|
590
|
+
__packager_asset: true,
|
|
591
|
+
name,
|
|
592
|
+
type: ext,
|
|
593
|
+
scales: [1],
|
|
594
|
+
httpServerLocation: httpLocation,
|
|
595
|
+
fileSystemLocation: dir,
|
|
596
|
+
hash: '',
|
|
597
|
+
width: undefined as number | undefined,
|
|
598
|
+
height: undefined as number | undefined,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// try to get image dimensions
|
|
602
|
+
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(ext)) {
|
|
603
|
+
try {
|
|
604
|
+
const { imageSize } = await import('image-size')
|
|
605
|
+
const dims = imageSize(id)
|
|
606
|
+
assetData.width = dims.width
|
|
607
|
+
assetData.height = dims.height
|
|
608
|
+
} catch {}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const code = `module.exports = require('react-native/Libraries/Image/AssetRegistry').registerAsset(${JSON.stringify(assetData)});`
|
|
612
|
+
|
|
613
|
+
return { code, moduleType: 'js' as any }
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* SWC transform for Hermes compatibility.
|
|
621
|
+
* Transforms class properties and private fields that Hermes doesn't support.
|
|
622
|
+
* Inspired by rollipop's swc-plugin.ts.
|
|
623
|
+
*/
|
|
624
|
+
function hermesCompatSWCPlugin(dev: boolean): Plugin {
|
|
625
|
+
let swc: typeof import('@swc/core') | null = null
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
name: 'vxrn:hermes-compat',
|
|
629
|
+
async transform(code, id) {
|
|
630
|
+
if (!/\.[cm]?[jt]sx?$/.test(id)) return
|
|
631
|
+
if (id.includes('\0') || id.includes('virtual:')) return
|
|
632
|
+
// skip files that don't need transformation
|
|
633
|
+
const hasClass = code.includes('class ') || code.includes('class{')
|
|
634
|
+
const hasAsync = !dev && code.includes('async ')
|
|
635
|
+
if (!hasClass && !hasAsync) return
|
|
636
|
+
// skip very large prebuilt files
|
|
637
|
+
if (code.length > 500_000) return
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
if (!swc) swc = await import('@swc/core')
|
|
641
|
+
|
|
642
|
+
// hermes needs class properties downleveled; prod also needs
|
|
643
|
+
// classes and async-to-generator for bytecode compilation
|
|
644
|
+
const envIncludes = [
|
|
645
|
+
'transform-class-properties',
|
|
646
|
+
'transform-class-static-block',
|
|
647
|
+
'transform-private-methods',
|
|
648
|
+
'transform-private-property-in-object',
|
|
649
|
+
...(!dev ? ['transform-classes', 'transform-async-to-generator'] : []),
|
|
650
|
+
]
|
|
651
|
+
|
|
652
|
+
const result = await swc.transform(code, {
|
|
653
|
+
filename: id,
|
|
654
|
+
configFile: false,
|
|
655
|
+
swcrc: false,
|
|
656
|
+
sourceMaps: false,
|
|
657
|
+
inputSourceMap: false,
|
|
658
|
+
env: {
|
|
659
|
+
targets: { node: 9999 },
|
|
660
|
+
include: envIncludes,
|
|
661
|
+
},
|
|
662
|
+
jsc: {
|
|
663
|
+
parser: { syntax: 'typescript', tsx: true },
|
|
664
|
+
transform: { react: { runtime: 'preserve' } },
|
|
665
|
+
externalHelpers: false,
|
|
666
|
+
assumptions: {
|
|
667
|
+
setPublicClassFields: true,
|
|
668
|
+
privateFieldsAsProperties: true,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
isModule: !id.endsWith('.cjs'),
|
|
672
|
+
})
|
|
673
|
+
return { code: result.code }
|
|
674
|
+
} catch (err: any) {
|
|
675
|
+
// don't crash on SWC transform errors (eg static blocks not supported)
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Run @react-native/babel-plugin-codegen on native component spec files.
|
|
683
|
+
* Without this, native components (RNSScreen, SafeAreaView, etc.) get
|
|
684
|
+
* "Codegen didn't run" warnings and may not render correctly.
|
|
685
|
+
*/
|
|
686
|
+
function codegenPlugin(): Plugin {
|
|
687
|
+
const NATIVE_COMPONENT_RE = /NativeComponent\.[jt]sx?$/
|
|
688
|
+
const SPEC_FILE_RE = /[/\\]specs?[/\\]/
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
name: 'vxrn:codegen',
|
|
692
|
+
async transform(code, id) {
|
|
693
|
+
if (!NATIVE_COMPONENT_RE.test(id) && !SPEC_FILE_RE.test(id)) return
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const babel = await import('@babel/core')
|
|
697
|
+
const result = await babel.transformAsync(code, {
|
|
698
|
+
filename: id,
|
|
699
|
+
babelrc: false,
|
|
700
|
+
configFile: false,
|
|
701
|
+
compact: false,
|
|
702
|
+
plugins: ['@react-native/babel-plugin-codegen'],
|
|
703
|
+
sourceType: 'unambiguous',
|
|
704
|
+
})
|
|
705
|
+
if (result?.code) return { code: result.code }
|
|
706
|
+
} catch {}
|
|
707
|
+
},
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Downgrade polyfill "not configurable" errors to warnings.
|
|
713
|
+
* hermes v1 has native fetch/Headers/etc that are non-configurable,
|
|
714
|
+
* so polyfill attempts throw errors that are noisy but harmless.
|
|
715
|
+
*/
|
|
716
|
+
function polyfillErrorDowngradePlugin(): Plugin {
|
|
717
|
+
return {
|
|
718
|
+
name: 'vxrn:polyfill-error-downgrade',
|
|
719
|
+
transform(code, id) {
|
|
720
|
+
if (!id.includes('node_modules')) return
|
|
721
|
+
if (!code.includes('console.error("Failed to set polyfill.')) return
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
code: code.replace(
|
|
725
|
+
/console\.error\(\s*"Failed to set polyfill\.\s*"\s*\+/g,
|
|
726
|
+
'console.warn("Failed to set polyfill. " +'
|
|
727
|
+
),
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Strip DevSettings reference in production builds.
|
|
735
|
+
* In production, NativeModules.getEnforcing('DevSettings') will fail
|
|
736
|
+
* since DevSettings is a dev-only module.
|
|
737
|
+
*/
|
|
738
|
+
function stripDevSettingsPlugin(dev: boolean): Plugin {
|
|
739
|
+
return {
|
|
740
|
+
name: 'vxrn:strip-dev-settings',
|
|
741
|
+
transform(code, id) {
|
|
742
|
+
if (dev) return
|
|
743
|
+
if (!code.includes('DevSettings')) return
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
code: code.replace(
|
|
747
|
+
/getEnforcing\s*\(\s*["']DevSettings["']\s*\)/g,
|
|
748
|
+
'patched_getEnforcing_DevSettings_will_not_work_in_production()'
|
|
749
|
+
),
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* React Refresh wrapper for native HMR.
|
|
757
|
+
* 1. Runs react-refresh/babel to add $RefreshReg$/$RefreshSig$ calls
|
|
758
|
+
* 2. Wraps each file with per-module $RefreshReg$ that includes the file path
|
|
759
|
+
* (react-refresh needs unique IDs like "filepath ComponentName")
|
|
760
|
+
* 3. Appends import.meta.hot.accept() for HMR boundaries
|
|
761
|
+
* 4. Schedules performReactRefresh() after module re-execution
|
|
762
|
+
*/
|
|
763
|
+
function nativeReactRefreshPlugin(): Plugin {
|
|
764
|
+
return {
|
|
765
|
+
name: 'vxrn:react-refresh',
|
|
766
|
+
async transform(code, id) {
|
|
767
|
+
// only wrap user app files (not node_modules, not generated entry, not virtual)
|
|
768
|
+
if (id.includes('node_modules')) return
|
|
769
|
+
if (id.includes('.vxrn-entry-native')) return
|
|
770
|
+
if (id.startsWith('\0')) return
|
|
771
|
+
if (!/\.[tj]sx?$/.test(id)) return
|
|
772
|
+
// skip files that clearly have no components (raw source before JSX transform)
|
|
773
|
+
// check for JSX syntax (<), function keyword, or arrow functions (=>)
|
|
774
|
+
if (!/[<]|function\s|=>\s*[{(]/.test(code)) return
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
// run react-refresh/babel to add $RefreshReg$ and $RefreshSig$
|
|
778
|
+
const babel = await import('@babel/core')
|
|
779
|
+
|
|
780
|
+
// babel needs parser plugins for TypeScript/JSX
|
|
781
|
+
const parserPlugins: any[] = []
|
|
782
|
+
if (id.endsWith('.tsx')) {
|
|
783
|
+
parserPlugins.push(['@babel/plugin-syntax-typescript', { isTSX: true }])
|
|
784
|
+
} else if (id.endsWith('.ts')) {
|
|
785
|
+
parserPlugins.push('@babel/plugin-syntax-typescript')
|
|
786
|
+
} else if (id.endsWith('.jsx')) {
|
|
787
|
+
parserPlugins.push('@babel/plugin-syntax-jsx')
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const result = await babel.transformAsync(code, {
|
|
791
|
+
filename: id,
|
|
792
|
+
babelrc: false,
|
|
793
|
+
configFile: false,
|
|
794
|
+
compact: false,
|
|
795
|
+
plugins: [...parserPlugins, 'react-refresh/babel'],
|
|
796
|
+
sourceType: 'unambiguous',
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
if (result?.code) {
|
|
800
|
+
// escape the id for use in string literal
|
|
801
|
+
const escapedId = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
802
|
+
|
|
803
|
+
// wrap with per-file $RefreshReg$ that includes the file path as unique ID
|
|
804
|
+
// and schedule performReactRefresh() after HMR patch re-execution
|
|
805
|
+
const wrappedCode = `
|
|
806
|
+
var __prevRefreshReg = globalThis.$RefreshReg$;
|
|
807
|
+
var __prevRefreshSig = globalThis.$RefreshSig$;
|
|
808
|
+
if (globalThis.__ReactRefresh) {
|
|
809
|
+
globalThis.$RefreshReg$ = function(type, id) {
|
|
810
|
+
globalThis.__ReactRefresh.register(type, "${escapedId}" + " " + id);
|
|
811
|
+
};
|
|
812
|
+
globalThis.$RefreshSig$ = globalThis.__ReactRefresh.createSignatureFunctionForTransform;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
${result.code}
|
|
816
|
+
|
|
817
|
+
globalThis.$RefreshReg$ = __prevRefreshReg;
|
|
818
|
+
globalThis.$RefreshSig$ = __prevRefreshSig;
|
|
819
|
+
if (import.meta.hot) {
|
|
820
|
+
import.meta.hot.accept(function() {
|
|
821
|
+
if (globalThis.__ReactRefresh) {
|
|
822
|
+
setTimeout(function() { globalThis.__ReactRefresh.performReactRefresh(); }, 30);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
`
|
|
827
|
+
return { code: wrappedCode }
|
|
828
|
+
}
|
|
829
|
+
} catch {
|
|
830
|
+
// if babel transform fails, just add the accept boundary
|
|
831
|
+
return {
|
|
832
|
+
code: code + `\nif (import.meta.hot) { import.meta.hot.accept(); }\n`,
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// --- HMR runtime ---
|
|
840
|
+
|
|
841
|
+
function getHmrRuntimeSource(): string {
|
|
842
|
+
return `
|
|
843
|
+
// vxrn HMR runtime for rolldown devMode
|
|
844
|
+
var BaseDevRuntime = DevRuntime;
|
|
845
|
+
|
|
846
|
+
class ReactNativeDevRuntime extends BaseDevRuntime {
|
|
847
|
+
constructor() {
|
|
848
|
+
var _shared = { _socket: null, _queue: [] };
|
|
849
|
+
var clientId = 'rn-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
850
|
+
super({ send: function(msg) {
|
|
851
|
+
var s = JSON.stringify(msg);
|
|
852
|
+
if (_shared._socket && _shared._socket.readyState === 1) { _shared._socket.send(s); }
|
|
853
|
+
else { _shared._queue.push(s); }
|
|
854
|
+
}}, clientId);
|
|
855
|
+
this._shared = _shared;
|
|
856
|
+
this._socket = null;
|
|
857
|
+
this._queue = [];
|
|
858
|
+
this.moduleHotContexts = {};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
createModuleHotContext(moduleId) {
|
|
862
|
+
var ctx = {
|
|
863
|
+
acceptCallbacks: [],
|
|
864
|
+
accept: function(cb) {
|
|
865
|
+
if (cb) ctx.acceptCallbacks.push({ deps: [moduleId], fn: cb });
|
|
866
|
+
},
|
|
867
|
+
invalidate: function() {},
|
|
868
|
+
on: function() {},
|
|
869
|
+
off: function() {},
|
|
870
|
+
send: function() {},
|
|
871
|
+
get refresh() { return globalThis.__ReactRefresh; },
|
|
872
|
+
get refreshUtils() {
|
|
873
|
+
return {
|
|
874
|
+
isReactRefreshBoundary: function(exports) {
|
|
875
|
+
if (!globalThis.__ReactRefresh) return false;
|
|
876
|
+
if (globalThis.__ReactRefresh.isLikelyComponentType(exports)) return true;
|
|
877
|
+
if (!exports || typeof exports !== 'object') return false;
|
|
878
|
+
var hasExports = false, allComponents = true;
|
|
879
|
+
for (var key in exports) {
|
|
880
|
+
hasExports = true;
|
|
881
|
+
if (key === '__esModule') continue;
|
|
882
|
+
if (!globalThis.__ReactRefresh.isLikelyComponentType(exports[key])) allComponents = false;
|
|
883
|
+
}
|
|
884
|
+
return hasExports && allComponents;
|
|
885
|
+
},
|
|
886
|
+
enqueueUpdate: function() {
|
|
887
|
+
if (globalThis.__ReactRefresh) {
|
|
888
|
+
setTimeout(function() { globalThis.__ReactRefresh.performReactRefresh(); }, 50);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
this.moduleHotContexts[moduleId] = ctx;
|
|
895
|
+
return ctx;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
applyUpdates(boundaries) {
|
|
899
|
+
for (var i = 0; i < boundaries.length; i++) {
|
|
900
|
+
var moduleId = boundaries[i][0];
|
|
901
|
+
var ctx = this.moduleHotContexts[moduleId];
|
|
902
|
+
if (ctx && ctx.acceptCallbacks) {
|
|
903
|
+
for (var j = 0; j < ctx.acceptCallbacks.length; j++) {
|
|
904
|
+
ctx.acceptCallbacks[j].fn(this.modules[moduleId].exports);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
setup(socket, origin) {
|
|
911
|
+
if (this._socket) return;
|
|
912
|
+
this._socket = socket;
|
|
913
|
+
// also set the shared messenger socket so queued messages can flush
|
|
914
|
+
if (this._shared) this._shared._socket = socket;
|
|
915
|
+
|
|
916
|
+
var flushQueues = function() {
|
|
917
|
+
// flush messenger queue
|
|
918
|
+
if (this._shared && this._shared._queue.length) {
|
|
919
|
+
for (var i = 0; i < this._shared._queue.length; i++) socket.send(this._shared._queue[i]);
|
|
920
|
+
this._shared._queue = [];
|
|
921
|
+
}
|
|
922
|
+
// flush instance queue
|
|
923
|
+
for (var i = 0; i < this._queue.length; i++) socket.send(this._queue[i]);
|
|
924
|
+
this._queue = [];
|
|
925
|
+
}.bind(this);
|
|
926
|
+
|
|
927
|
+
if (socket.readyState === 1) {
|
|
928
|
+
flushQueues();
|
|
929
|
+
} else {
|
|
930
|
+
socket.addEventListener('open', function() {
|
|
931
|
+
flushQueues();
|
|
932
|
+
}, { once: true });
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// HMR message handling is done by the outro WebSocket handler
|
|
936
|
+
// the runtime's setup() only needs to flush queued messages
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
globalThis.__rolldown_runtime__ = new ReactNativeDevRuntime();
|
|
941
|
+
`
|
|
942
|
+
}
|