vxrn 2.10.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/config/getBaseVitePlugins.mjs +3 -1
  2. package/dist/config/getBaseVitePlugins.mjs.map +1 -1
  3. package/dist/config/getBaseVitePlugins.native.js +3 -1
  4. package/dist/config/getBaseVitePlugins.native.js.map +1 -1
  5. package/dist/constants.mjs +23 -1
  6. package/dist/constants.mjs.map +1 -1
  7. package/dist/constants.native.js +23 -1
  8. package/dist/constants.native.js.map +1 -1
  9. package/dist/exports/build.mjs +6 -4
  10. package/dist/exports/build.mjs.map +1 -1
  11. package/dist/exports/build.native.js +6 -4
  12. package/dist/exports/build.native.js.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/index.mjs +2 -1
  15. package/dist/index.native.js +2 -1
  16. package/dist/plugins/reactNativeDevServer.mjs +7 -5
  17. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  18. package/dist/plugins/reactNativeDevServer.native.js +8 -5
  19. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  20. package/dist/rn-commands/bundle/buildBundle.mjs +9 -0
  21. package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
  22. package/dist/rn-commands/bundle/buildBundle.native.js +9 -0
  23. package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
  24. package/dist/runtime/native-prelude.mjs +7 -1
  25. package/dist/runtime/native-prelude.mjs.map +1 -1
  26. package/dist/runtime/native-prelude.native.js +7 -1
  27. package/dist/runtime/native-prelude.native.js.map +1 -1
  28. package/dist/utils/compileNativeDevBytecode.mjs +13 -3
  29. package/dist/utils/compileNativeDevBytecode.mjs.map +1 -1
  30. package/dist/utils/compileNativeDevBytecode.native.js +13 -3
  31. package/dist/utils/compileNativeDevBytecode.native.js.map +1 -1
  32. package/dist/utils/createNativeDevEngine.mjs +375 -173
  33. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  34. package/dist/utils/createNativeDevEngine.native.js +429 -193
  35. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  36. package/dist/utils/environmentUtils.mjs +1 -1
  37. package/dist/utils/environmentUtils.mjs.map +1 -1
  38. package/dist/utils/environmentUtils.native.js +1 -1
  39. package/dist/utils/environmentUtils.native.js.map +1 -1
  40. package/dist/utils/scanDepsToOptimize.mjs +37 -15
  41. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  42. package/dist/utils/scanDepsToOptimize.native.js +45 -15
  43. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  44. package/package.json +13 -13
  45. package/src/config/getBaseVitePlugins.ts +2 -0
  46. package/src/constants.ts +24 -0
  47. package/src/exports/build.lifecycle.test.ts +2 -1
  48. package/src/exports/build.ts +11 -4
  49. package/src/index.ts +1 -0
  50. package/src/plugins/reactNativeCommonJsPlugin.ts +1 -1
  51. package/src/plugins/reactNativeDevServer.ts +19 -8
  52. package/src/rn-commands/bundle/buildBundle.ts +13 -1
  53. package/src/runtime/native-prelude.ts +7 -1
  54. package/src/types.ts +1 -1
  55. package/src/utils/compileNativeDevBytecode.test.ts +8 -8
  56. package/src/utils/compileNativeDevBytecode.ts +22 -2
  57. package/src/utils/createNativeDevEngine.test.ts +52 -25
  58. package/src/utils/createNativeDevEngine.ts +567 -254
  59. package/src/utils/environmentUtils.ts +6 -1
  60. package/src/utils/scanDepsToOptimize.test.ts +46 -2
  61. package/src/utils/scanDepsToOptimize.ts +78 -18
  62. package/types/config/getBaseVitePlugins.d.ts.map +1 -1
  63. package/types/constants.d.ts +2 -0
  64. package/types/constants.d.ts.map +1 -1
  65. package/types/exports/build.d.ts.map +1 -1
  66. package/types/index.d.ts +1 -0
  67. package/types/index.d.ts.map +1 -1
  68. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  69. package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
  70. package/types/runtime/native-prelude.d.ts.map +1 -1
  71. package/types/types.d.ts +1 -1
  72. package/types/types.d.ts.map +1 -1
  73. package/types/utils/compileNativeDevBytecode.d.ts +2 -1
  74. package/types/utils/compileNativeDevBytecode.d.ts.map +1 -1
  75. package/types/utils/createNativeDevEngine.d.ts +11 -5
  76. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  77. package/types/utils/environmentUtils.d.ts.map +1 -1
  78. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
@@ -77,14 +77,19 @@ export const build = async (optionsIn: VXRNOptions, buildArgs: BuildArgs = {}) =
77
77
 
78
78
  const outDir = userViteConfig?.build?.outDir ?? 'dist'
79
79
 
80
- const isNativeBuild = buildArgs.platform === 'ios' || buildArgs.platform === 'android'
80
+ const platform = buildArgs.platform
81
+ const isNativeBuild =
82
+ platform === 'ios' ||
83
+ platform === 'android' ||
84
+ platform === 'macos' ||
85
+ platform === 'linux'
81
86
 
82
87
  // generation reuses client assets and their manifest, but rebuilds the server
83
88
  const cleanDir =
84
89
  buildArgs.step === 'generate' && !isNativeBuild ? `${outDir}/server` : outDir
85
90
  await rm(cleanDir, { recursive: true, force: true })
86
91
 
87
- if (buildArgs.platform === 'ios' || buildArgs.platform === 'android') {
92
+ if (isNativeBuild) {
88
93
  const { buildBundle } = await import('../rn-commands/bundle/buildBundle')
89
94
 
90
95
  return buildBundle(
@@ -94,8 +99,10 @@ export const build = async (optionsIn: VXRNOptions, buildArgs: BuildArgs = {}) =
94
99
  // default (./src/entry-native.tsx) exists only in plain vxrn apps.
95
100
  { root: options.root, vxrnEntries: optionsIn.entries },
96
101
  {
97
- platform: buildArgs.platform,
98
- bundleOutput: `${outDir}${sep}${buildArgs.platform}.js`,
102
+ platform,
103
+ bundleOutput: `${outDir}${sep}${platform}.js`,
104
+ // every native build emits imported assets beside its bundle
105
+ assetsDest: outDir,
99
106
  dev: false,
100
107
  entryFile: '',
101
108
  resetCache: true,
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export * from './utils/environmentUtils'
22
22
  export * from './utils/getServerEntry'
23
23
  export * from './utils/nearestPackageJson'
24
24
  export * from './utils/swiftPackageId'
25
+ export { resolveNpmHermesc } from './utils/compileNativeDevBytecode'
25
26
 
26
27
  export * from './plugins/rollupRemoveUnusedImports'
27
28
  export * from './plugins/autoDepOptimizePlugin'
@@ -12,7 +12,7 @@ import { isNativeEnvironment } from '../utils/environmentUtils'
12
12
  * * https://reactnative.dev/docs/platform-specific-code#platform-specific-extensions
13
13
  * * https://v5.vite.dev/config/shared-options.html#resolve-extensions
14
14
  */
15
- const getNativeExtensions = (platform: 'ios' | 'android') => {
15
+ const getNativeExtensions = (platform: 'ios' | 'android' | 'macos' | 'linux') => {
16
16
  return [
17
17
  `.${platform}.tsx`,
18
18
  `.${platform}.ts`,
@@ -5,6 +5,7 @@ import {
5
5
  expoClientFromNativeApp,
6
6
  type NativeAppManifest,
7
7
  } from '@vxrn/utils/nativeAppManifest'
8
+ import type { NativePlatform } from '@vxrn/utils/platformEnv'
8
9
  import {
9
10
  addConnectedNativeClient,
10
11
  removeConnectedNativeClient,
@@ -35,7 +36,7 @@ type ClientMessage = {
35
36
 
36
37
  type NativeHmrSocket = WebSocket & {
37
38
  vxrnClientId: string
38
- vxrnPlatform: 'ios' | 'android'
39
+ vxrnPlatform: NativePlatform
39
40
  }
40
41
 
41
42
  // metro's asset server answers `/assets/<path>` for any asset file inside the
@@ -102,9 +103,11 @@ export function createReactNativeDevServerPlugin(
102
103
  const hmrWSS = new WebSocketServer({ noServer: true })
103
104
  const clientWSS = new WebSocketServer({ noServer: true })
104
105
  const messageWSS = createMessageSocket()
105
- const validPlatforms: Record<string, 'ios' | 'android' | undefined> = {
106
+ const validPlatforms: Record<string, NativePlatform | undefined> = {
106
107
  ios: 'ios',
107
108
  android: 'android',
109
+ macos: 'macos',
110
+ linux: 'linux',
108
111
  }
109
112
  const devEngines: Record<
110
113
  string,
@@ -112,8 +115,10 @@ export function createReactNativeDevServerPlugin(
112
115
  > = {}
113
116
  const devEngineCreating: Record<string, Promise<unknown> | null> = {}
114
117
  const warnedProdBundleRequest = new Set<string>()
115
- const pendingReloadPlatforms = new Set<'ios' | 'android'>()
116
- let compileBytecode: ReturnType<typeof nativeDevBytecodeCompiler> | undefined
118
+ const pendingReloadPlatforms = new Set<NativePlatform>()
119
+ const compileBytecode: Partial<
120
+ Record<'ios' | 'macos', ReturnType<typeof nativeDevBytecodeCompiler>>
121
+ > = {}
117
122
 
118
123
  const devToolsSocketEndpoints = ['/inspector/device', '/inspector/debug']
119
124
  const reactNativeDevToolsUrl = `http://${typeof host === 'string' && host !== '0.0.0.0' ? host : 'localhost'}:${getBoundPort(server)}`
@@ -491,7 +496,7 @@ export function createReactNativeDevServerPlugin(
491
496
  })
492
497
  })
493
498
 
494
- const getDevEngine = async (platform: 'ios' | 'android') => {
499
+ const getDevEngine = async (platform: NativePlatform) => {
495
500
  while (!devEngines[platform]) {
496
501
  // prevent duplicate creation from concurrent requests
497
502
  if (!devEngineCreating[platform]) {
@@ -612,9 +617,15 @@ export function createReactNativeDevServerPlugin(
612
617
  try {
613
618
  const bundle = await (await getDevEngine(platform)).getBundle()
614
619
  let body: string | Buffer = bundle.code
615
- if (platform === 'ios' && url.searchParams.get('bytecode') === 'hermes') {
616
- compileBytecode ||= nativeDevBytecodeCompiler(root)
617
- body = await compileBytecode(
620
+ if (
621
+ (platform === 'ios' || platform === 'macos') &&
622
+ url.searchParams.get('bytecode') === 'hermes'
623
+ ) {
624
+ const compile = (compileBytecode[platform] ||= nativeDevBytecodeCompiler(
625
+ root,
626
+ platform
627
+ ))
628
+ body = await compile(
618
629
  bundle,
619
630
  `${url.origin}/index.bundle?platform=${platform}`
620
631
  )
@@ -2,6 +2,7 @@ import FSExtra from 'fs-extra'
2
2
  import { clearTransformCache } from '@vxrn/compiler'
3
3
  import type { BundleCommandArgs } from '@vxrn/vite-plugin-metro/rn-commands'
4
4
  import { resolveConfig } from 'vite'
5
+ import { dirname, join, resolve } from 'node:path'
5
6
  import { loadEnv } from '../../exports/loadEnv'
6
7
  import { buildNativeBundle } from '../../utils/createNativeDevEngine'
7
8
 
@@ -14,7 +15,7 @@ export async function buildBundle(
14
15
  bundleImpl: any = null
15
16
  ): Promise<void> {
16
17
  const {
17
- platform, // Android is not supported yet.
18
+ platform,
18
19
  dev,
19
20
  bundleOutput,
20
21
  // React Native's `--bundle-encoding` names a Node Buffer encoding.
@@ -92,6 +93,17 @@ export async function buildBundle(
92
93
  })
93
94
  const builtBundle = result.code
94
95
 
96
+ // gradle sends drawable resources to assetsDest but packages arbitrary files
97
+ // from the bundle directory. DOM pages must share that assets root with JS.
98
+ if (platform === 'android' && assetsDest) {
99
+ const source = join(assetsDest, 'one-dom')
100
+ const destination = join(dirname(bundleOutput), 'one-dom')
101
+ if (resolve(source) !== resolve(destination)) {
102
+ FSExtra.removeSync(destination)
103
+ if (FSExtra.existsSync(source)) FSExtra.moveSync(source, destination)
104
+ }
105
+ }
106
+
95
107
  // write sourcemap if available and requested
96
108
  if (result.map && args.sourcemapOutput) {
97
109
  FSExtra.outputFileSync(args.sourcemapOutput, result.map, { encoding: 'utf8' })
@@ -33,7 +33,13 @@ global.performance = global.performance || { now: function() { return Date.now()
33
33
  // hermes can expose addEventListener without the matching removal API. browser
34
34
  // libraries use addEventListener as their capability check, then crash during
35
35
  // cleanup. keep the incomplete host API from advertising browser event support.
36
- if (typeof global.addEventListener === 'function' && typeof global.removeEventListener !== 'function') {
36
+ // the bundle runs at script scope, so a module's own top-level
37
+ // function addEventListener declaration is also this global; only a native host function is cleared.
38
+ if (
39
+ typeof global.addEventListener === 'function' &&
40
+ typeof global.removeEventListener !== 'function' &&
41
+ Function.prototype.toString.call(global.addEventListener).indexOf('[native code]') !== -1
42
+ ) {
37
43
  global.addEventListener = undefined;
38
44
  }
39
45
 
package/src/types.ts CHANGED
@@ -11,7 +11,7 @@ export type BuildArgs = {
11
11
  step?: string
12
12
  only?: string
13
13
  analyze?: boolean
14
- platform?: 'ios' | 'web' | 'android'
14
+ platform?: 'ios' | 'web' | 'android' | 'macos' | 'linux'
15
15
  }
16
16
 
17
17
  export type AfterBuildProps = {
@@ -46,7 +46,7 @@ it.each(['prebuilt', 'source'])(
46
46
  },
47
47
  })
48
48
  )
49
- const compiler = nativeDevBytecodeCompiler(root)
49
+ const compiler = nativeDevBytecodeCompiler(root, 'ios')
50
50
  const compile = (bundle: { code: string; map: string }) =>
51
51
  compiler(bundle, 'http://localhost:8081/index.bundle?platform=ios')
52
52
  const first = { code: 'globalThis.answer = 42;', map: '' }
@@ -87,7 +87,7 @@ it('uses HERMESC_PATH without any ios/Pods', async () => {
87
87
  const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-override-'))
88
88
  process.env.HERMESC_PATH = hostHermesc()
89
89
  try {
90
- const compiler = nativeDevBytecodeCompiler(root)
90
+ const compiler = nativeDevBytecodeCompiler(root, 'ios')
91
91
  const bytes = await compiler(
92
92
  { code: 'globalThis.answer = 42;', map: '' },
93
93
  'http://localhost:8081/index.bundle?platform=ios'
@@ -102,10 +102,10 @@ it('treats empty HERMESC_PATH as unset', async () => {
102
102
  const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-empty-'))
103
103
  process.env.HERMESC_PATH = ' '
104
104
  try {
105
- expect(() => nativeDevBytecodeCompiler(root)).toThrow(
105
+ expect(() => nativeDevBytecodeCompiler(root, 'ios')).toThrow(
106
106
  /hermes-engine\.podspec\.json/
107
107
  )
108
- expect(() => nativeDevBytecodeCompiler(root)).not.toThrow(/HERMESC_PATH is set/)
108
+ expect(() => nativeDevBytecodeCompiler(root, 'ios')).not.toThrow(/HERMESC_PATH is set/)
109
109
  } finally {
110
110
  await rm(root, { recursive: true, force: true })
111
111
  }
@@ -115,7 +115,7 @@ it('loudly rejects a HERMESC_PATH that points nowhere', async () => {
115
115
  const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-invalid-'))
116
116
  process.env.HERMESC_PATH = join(root, 'no-such-hermesc')
117
117
  try {
118
- expect(() => nativeDevBytecodeCompiler(root)).toThrow(
118
+ expect(() => nativeDevBytecodeCompiler(root, 'ios')).toThrow(
119
119
  /HERMESC_PATH is set but points nowhere/
120
120
  )
121
121
  } finally {
@@ -133,7 +133,7 @@ it.runIf(process.platform !== 'win32')(
133
133
  await chmod(plain, 0o644)
134
134
  process.env.HERMESC_PATH = plain
135
135
  try {
136
- expect(() => nativeDevBytecodeCompiler(root)).toThrow(
136
+ expect(() => nativeDevBytecodeCompiler(root, 'ios')).toThrow(
137
137
  /HERMESC_PATH is not executable/
138
138
  )
139
139
  } finally {
@@ -145,7 +145,7 @@ it.runIf(process.platform !== 'win32')(
145
145
  it('loudly rejects a root without ios/Pods when the override is unset', async () => {
146
146
  const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-nopods-'))
147
147
  try {
148
- expect(() => nativeDevBytecodeCompiler(root)).toThrow(
148
+ expect(() => nativeDevBytecodeCompiler(root, 'ios')).toThrow(
149
149
  /hermes-engine\.podspec\.json.*HERMESC_PATH/s
150
150
  )
151
151
  } finally {
@@ -159,7 +159,7 @@ it('loudly rejects a malformed podspec when the override is unset', async () =>
159
159
  const specs = join(root, 'ios/Pods/Local Podspecs')
160
160
  await mkdir(specs, { recursive: true })
161
161
  await writeFile(join(specs, 'hermes-engine.podspec.json'), '{oops')
162
- expect(() => nativeDevBytecodeCompiler(root)).toThrow(
162
+ expect(() => nativeDevBytecodeCompiler(root, 'ios')).toThrow(
163
163
  /hermes-engine\.podspec\.json/
164
164
  )
165
165
  } finally {
@@ -1,5 +1,6 @@
1
1
  import { accessSync, constants, readFileSync, statSync } from 'node:fs'
2
2
  import { execFile } from 'node:child_process'
3
+ import { createRequire } from 'node:module'
3
4
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4
5
  import { tmpdir } from 'node:os'
5
6
  import { dirname, join } from 'node:path'
@@ -77,10 +78,29 @@ function resolvePodHermesc(root: string): string {
77
78
  return cliPath.replace(/\$\(PODS_ROOT\)|\$\{PODS_ROOT\}/g, pods)
78
79
  }
79
80
 
81
+ // through react-native's own dependency graph: it pins the compiler whose
82
+ // bytecode its hermes vm runs. the macos dev server and builds without a
83
+ // hermes-engine pod compile with it.
84
+ export function resolveNpmHermesc(root: string): string {
85
+ const require = createRequire(join(root, 'package.json'))
86
+ const reactNativeJson = require.resolve('react-native/package.json')
87
+ const nested = createRequire(reactNativeJson)
88
+ const compilerJson = nested.resolve('hermes-compiler/package.json')
89
+ const dir = compilerJson.slice(0, -'package.json'.length)
90
+ const bin =
91
+ process.platform === 'darwin'
92
+ ? 'osx-bin'
93
+ : process.platform === 'win32'
94
+ ? 'win64-bin'
95
+ : 'linux64-bin'
96
+ return join(dir, 'hermesc', bin, process.platform === 'win32' ? 'hermesc.exe' : 'hermesc')
97
+ }
98
+
80
99
  // compile once per emitted bundle. the device can start a fresh hermes context
81
100
  // without compiling every cold route's module closures on its js thread.
82
- export function nativeDevBytecodeCompiler(root: string) {
83
- const compiler = useHermescOverride() ?? resolvePodHermesc(root)
101
+ export function nativeDevBytecodeCompiler(root: string, platform: 'ios' | 'macos') {
102
+ const compiler =
103
+ useHermescOverride() ?? (platform === 'ios' ? resolvePodHermesc(root) : resolveNpmHermesc(root))
84
104
  const compiled = new WeakMap<NativeDevBundle, Map<string, Promise<Buffer>>>()
85
105
 
86
106
  return (bundle: NativeDevBundle, sourceURL: string): Promise<Buffer> => {
@@ -32,6 +32,8 @@ import {
32
32
  normalizeNativeCommonJSInterop,
33
33
  postProcessNativeBundle,
34
34
  vxrnCompilerPlugin,
35
+ NATIVE_DEV_RUNTIME_END,
36
+ NATIVE_MODULE_SCOPE_MARKER,
35
37
  wrapNativeBundleModuleScope,
36
38
  type NativePluginContext,
37
39
  } from './createNativeDevEngine'
@@ -218,8 +220,20 @@ describe.each(['ios', 'android'] as const)('native plugin adapters on %s', (plat
218
220
  }
219
221
  runInNewContext(code, context)
220
222
  expect(Reflect.get(context, 'nativePluginResult')).toEqual([
221
- { name: 'global', root, platform, dev: mode === 'dev' },
222
- { name: 'direct', root, platform, dev: mode === 'dev' },
223
+ {
224
+ name: 'global',
225
+ root,
226
+ platform,
227
+ dev: mode === 'dev',
228
+ command: mode === 'dev' ? 'serve' : 'build',
229
+ },
230
+ {
231
+ name: 'direct',
232
+ root,
233
+ platform,
234
+ dev: mode === 'dev',
235
+ command: mode === 'dev' ? 'serve' : 'build',
236
+ },
223
237
  'plain',
224
238
  ])
225
239
  for (const factory of factories) {
@@ -227,6 +241,7 @@ describe.each(['ios', 'android'] as const)('native plugin adapters on %s', (plat
227
241
  root,
228
242
  platform,
229
243
  dev: mode === 'dev',
244
+ command: mode === 'dev' ? 'serve' : 'build',
230
245
  })
231
246
  }
232
247
  } finally {
@@ -727,8 +742,9 @@ describe('native prelude', () => {
727
742
  })
728
743
 
729
744
  it('does not advertise a host event API that cannot remove listeners', () => {
745
+ // a bound function prints as [native code], like a host function
730
746
  const context = {
731
- addEventListener() {},
747
+ addEventListener: function () {}.bind(null),
732
748
  }
733
749
 
734
750
  runInNewContext(getNativePrelude({ dev: false, platform: 'ios' }), context)
@@ -736,6 +752,15 @@ describe('native prelude', () => {
736
752
  expect(Reflect.get(context, 'addEventListener')).toBeUndefined()
737
753
  })
738
754
 
755
+ it('keeps a javascript addEventListener without a removal API', () => {
756
+ const addEventListener = function addEventListener() {}
757
+ const context = { addEventListener }
758
+
759
+ runInNewContext(getNativePrelude({ dev: false, platform: 'ios' }), context)
760
+
761
+ expect(Reflect.get(context, 'addEventListener')).toBe(addEventListener)
762
+ })
763
+
739
764
  it('preserves a complete host event API', () => {
740
765
  const addEventListener = () => {}
741
766
  const removeEventListener = () => {}
@@ -1708,30 +1733,32 @@ describe('getNativeTransformConfig platform env defines', () => {
1708
1733
  })
1709
1734
 
1710
1735
  describe('wrapNativeBundleModuleScope', () => {
1711
- // matches the marker rolldown dev() emits at the start of the runtime region
1712
- const RUNTIME_MARKER = '//#region \\0rolldown/runtime.js'
1713
-
1714
- it('wraps module code after the prelude so top-level vars do not leak to global', () => {
1715
- const prelude = 'globalThis.global = globalThis;\nglobalThis.__DEV__ = true;\n'
1736
+ it.each([
1737
+ ['rolldown 1.2.15', '// \\0rolldown/runtime.js'],
1738
+ ['rolldown 1.2.13', '//#region \\0rolldown/runtime.js'],
1739
+ ])('wraps module code after the prelude with %s module comments', (_, header) => {
1740
+ const prelude = `globalThis.global = globalThis;\nglobalThis.__DEV__ = true;\n${NATIVE_MODULE_SCOPE_MARKER}\n`
1716
1741
  // a top-level `var Headers` in a script becomes a non-configurable global,
1717
1742
  // which is the exact leak that breaks RN's polyfillGlobal in dev
1718
- const body = `${RUNTIME_MARKER}\nvar fetch_hot, fetch$1, Headers, Request, Response$1;\nglobalThis.__rolldown_runtime__ = {};\n`
1719
-
1720
- const out = wrapNativeBundleModuleScope(prelude + body)
1721
-
1722
- const openIdx = out.indexOf(';(function() {')
1723
- expect(openIdx).toBeGreaterThan(-1)
1724
- // prelude (global setup) stays at script scope, before the wrap opens
1725
- expect(out.indexOf('globalThis.__DEV__')).toBeLessThan(openIdx)
1726
- // the leaking declaration is now inside the function scope
1727
- expect(out.indexOf('var fetch_hot')).toBeGreaterThan(openIdx)
1728
- // and the result must still be syntactically valid (balanced wrap)
1729
- expect(() => new Function(out)).not.toThrow()
1743
+ const body = `${header}\nvar fetch_hot, fetch$1, Headers, Request, Response$1;\nglobalThis.__rolldown_runtime__ = {};\n`
1744
+ const input = prelude + body
1745
+
1746
+ const out = wrapNativeBundleModuleScope(input)
1747
+
1748
+ const context = createContext({})
1749
+ runInContext(out, context)
1750
+ expect(runInContext('globalThis.__DEV__', context)).toBe(true)
1751
+ expect(
1752
+ runInContext('Object.getOwnPropertyDescriptor(globalThis, "Headers")', context)
1753
+ ).toBeUndefined()
1754
+ // the opener shares the marker line, so every module keeps its mapped line
1755
+ const lineOf = (code: string) =>
1756
+ code.split('\n').indexOf('var fetch_hot, fetch$1, Headers, Request, Response$1;')
1757
+ expect(lineOf(out)).toBe(lineOf(input))
1730
1758
  })
1731
1759
 
1732
- it('is a no-op when the runtime marker is absent (e.g. prod bundle)', () => {
1733
- const input = 'var x = 1;\nconsole.log(x);\n'
1734
- expect(wrapNativeBundleModuleScope(input)).toBe(input)
1760
+ it('fails the build when the prelude marker is missing', () => {
1761
+ expect(() => wrapNativeBundleModuleScope('var x = 1;\n')).toThrow('script scope')
1735
1762
  })
1736
1763
  })
1737
1764
 
@@ -3507,9 +3534,9 @@ describe('native dev bundle post-processing failures', () => {
3507
3534
  // downleveled, which throws inside handleOutput.
3508
3535
  return {
3509
3536
  code: [
3510
- '//#region \\0rolldown/runtime.js',
3537
+ NATIVE_MODULE_SCOPE_MARKER,
3511
3538
  'class Poison { x = 1; y = 2; z = 3; }',
3512
- '//#endregion',
3539
+ NATIVE_DEV_RUNTIME_END,
3513
3540
  'globalThis.__postprocessProbe = new Poison();',
3514
3541
  '',
3515
3542
  ].join('\n'),