vxrn 2.13.10 → 2.14.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.
package/expo-plugin.cjs CHANGED
@@ -898,11 +898,24 @@ extern void OneSwiftRegisterPackage(const char *name, int (*entry)(int, char **)
898
898
  return nextConfig
899
899
  },
900
900
  ],
901
+ [
902
+ withGradleProperties,
903
+ (nextConfig) => {
904
+ // MainActivity opts into androidx edge-to-edge; this tells react
905
+ // native the window is edge-to-edge on every api level.
906
+ const properties = nextConfig.modResults.filter(
907
+ (item) => !(item.type === 'property' && item.key === 'edgeToEdgeEnabled')
908
+ )
909
+ properties.push({ type: 'property', key: 'edgeToEdgeEnabled', value: 'true' })
910
+ nextConfig.modResults = properties
911
+ return nextConfig
912
+ },
913
+ ],
901
914
  [
902
915
  withMainActivity,
903
916
  (nextConfig) => {
904
- const contents = nativeProjectPatches.addReactNativeScreensFix(
905
- nextConfig.modResults.contents
917
+ const contents = nativeProjectPatches.enableEdgeToEdgeInMainActivity(
918
+ nativeProjectPatches.addReactNativeScreensFix(nextConfig.modResults.contents)
906
919
  )
907
920
  nextConfig.modResults.contents = options.holdLaunchScreen
908
921
  ? nativeProjectPatches.holdLaunchScreenInMainActivity(contents)
@@ -704,6 +704,47 @@ function addReactNativeScreensFix(input) {
704
704
  return input
705
705
  }
706
706
 
707
+ // edge-to-edge status bars follow the system appearance: AppCompat's DayNight
708
+ // theme leaves the status bar icon style at its white default, so every One
709
+ // android activity opts into androidx edge-to-edge like a native app. runs
710
+ // after addReactNativeScreensFix, which guarantees onCreate exists.
711
+ const EDGE_TO_EDGE_KT_IMPORT = 'import androidx.activity.enableEdgeToEdge'
712
+ const EDGE_TO_EDGE_JAVA_IMPORT = 'import androidx.activity.EdgeToEdge;'
713
+
714
+ function enableEdgeToEdgeInMainActivity(mainActivity) {
715
+ if (
716
+ mainActivity.includes(EDGE_TO_EDGE_KT_IMPORT) ||
717
+ mainActivity.includes(EDGE_TO_EDGE_JAVA_IMPORT)
718
+ ) {
719
+ return mainActivity
720
+ }
721
+ const isKotlin = mainActivity.includes('class MainActivity : ReactActivity()')
722
+ if (isKotlin) {
723
+ if (!SUPER_ON_CREATE.test(mainActivity)) {
724
+ throw new Error(
725
+ '[vxrn] MainActivity.kt changed shape: cannot enable edge-to-edge before super.onCreate'
726
+ )
727
+ }
728
+ return mainActivity
729
+ .replace(/package\s+[\w.]+/, '$&\n' + EDGE_TO_EDGE_KT_IMPORT)
730
+ .replace(
731
+ SUPER_ON_CREATE,
732
+ '$1// [vxrn/one] status bar icons follow the system appearance\n$1enableEdgeToEdge()\n$1super.onCreate(null)'
733
+ )
734
+ }
735
+ if (!/^([ \t]*)super\.onCreate\(savedInstanceState\);$/m.test(mainActivity)) {
736
+ throw new Error(
737
+ '[vxrn] MainActivity.java changed shape: cannot enable edge-to-edge before super.onCreate'
738
+ )
739
+ }
740
+ return mainActivity
741
+ .replace(/package\s+[\w.]+;/, '$&\n' + EDGE_TO_EDGE_JAVA_IMPORT)
742
+ .replace(
743
+ /^([ \t]*)super\.onCreate\(savedInstanceState\);$/m,
744
+ '$1// [vxrn/one] status bar icons follow the system appearance\n$1EdgeToEdge.enable(this);\n$1super.onCreate(savedInstanceState);'
745
+ )
746
+ }
747
+
707
748
  /**
708
749
  * swift packages in the app (a directory with Package.swift whose view a
709
750
  * react native screen imports) are generated by prebuild as one local pod each
@@ -849,4 +890,5 @@ module.exports = {
849
890
  replaceAppBuildGradleReactBlock,
850
891
  addDepsPatchToAppBuildGradle,
851
892
  addReactNativeScreensFix,
893
+ enableEdgeToEdgeInMainActivity,
852
894
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vxrn",
3
- "version": "2.13.10",
3
+ "version": "2.14.1",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -80,14 +80,14 @@
80
80
  "@react-native-community/cli": "^20.2.0",
81
81
  "@react-native-community/cli-config": "^20.2.0",
82
82
  "@react-native/dev-middleware": "^0.88.0",
83
- "@vxrn/compiler": "2.13.10",
84
- "@vxrn/debug": "2.13.10",
85
- "@vxrn/query-string": "2.13.10",
86
- "@vxrn/resolve": "2.13.10",
87
- "@vxrn/url-parse": "2.13.10",
88
- "@vxrn/utils": "2.13.10",
89
- "@vxrn/vendor": "2.13.10",
90
- "@vxrn/vite-flow": "2.13.10",
83
+ "@vxrn/compiler": "2.14.1",
84
+ "@vxrn/debug": "2.14.1",
85
+ "@vxrn/query-string": "2.14.1",
86
+ "@vxrn/resolve": "2.14.1",
87
+ "@vxrn/url-parse": "2.14.1",
88
+ "@vxrn/utils": "2.14.1",
89
+ "@vxrn/vendor": "2.14.1",
90
+ "@vxrn/vite-flow": "2.14.1",
91
91
  "citty": "^0.1.6",
92
92
  "dotenv": "^17.2.1",
93
93
  "dotenv-expand": "^12.0.2",
@@ -121,20 +121,20 @@
121
121
  "react-native": "0.88.0",
122
122
  "typescript": "7.0.2",
123
123
  "vitest": "^4.1.0",
124
- "@vxrn/vite-plugin-metro": "2.13.10"
124
+ "@vxrn/vite-plugin-metro": "2.14.1"
125
125
  },
126
126
  "publishConfig": {
127
127
  "access": "public"
128
128
  },
129
129
  "peerDependencies": {
130
- "@vxrn/vite-plugin-metro": "2.13.10"
130
+ "@vxrn/vite-plugin-metro": "2.14.1"
131
131
  },
132
132
  "peerDependenciesMeta": {
133
133
  "@vxrn/vite-plugin-metro": {
134
134
  "optional": true
135
135
  }
136
136
  },
137
- "releaseSourceCommit": "10ae059cc30ab9f70a12d9711ba8346d596d61d2",
137
+ "releaseSourceCommit": "e780d1ad0bdf938d0e21a5cdff99a0cc78184030",
138
138
  "repository": {
139
139
  "type": "git",
140
140
  "url": "git+https://github.com/onejs/one.git",
@@ -131,6 +131,37 @@ class MainActivity : ReactActivity() {
131
131
  expect(result).toContain('RNScreensFragmentFactory')
132
132
  expect(result).toContain('super.onCreate(null)')
133
133
  })
134
+
135
+ it('enables edge-to-edge in MainActivity', () => {
136
+ const kotlin = patches.enableEdgeToEdgeInMainActivity(
137
+ patches.addReactNativeScreensFix(`
138
+ package dev.one.test
139
+ import com.facebook.react.ReactActivity
140
+ class MainActivity : ReactActivity() {
141
+ }
142
+ `)
143
+ )
144
+
145
+ expect(kotlin).toContain('import androidx.activity.enableEdgeToEdge')
146
+ expect(kotlin).toContain('enableEdgeToEdge()')
147
+ expect(kotlin.indexOf('enableEdgeToEdge()')).toBeLessThan(
148
+ kotlin.indexOf('super.onCreate(null)')
149
+ )
150
+ expect(patches.enableEdgeToEdgeInMainActivity(kotlin)).toBe(kotlin)
151
+
152
+ const java = patches.enableEdgeToEdgeInMainActivity(
153
+ patches.addReactNativeScreensFix(`
154
+ package dev.one.test;
155
+ import com.facebook.react.ReactActivity;
156
+ public class MainActivity extends ReactActivity {
157
+ }
158
+ `)
159
+ )
160
+
161
+ expect(java).toContain('import androidx.activity.EdgeToEdge;')
162
+ expect(java).toContain('EdgeToEdge.enable(this);')
163
+ expect(patches.enableEdgeToEdgeInMainActivity(java)).toBe(java)
164
+ })
134
165
  })
135
166
 
136
167
  function expoProject() {
@@ -282,6 +313,27 @@ describe('vxrn/expo-plugin', () => {
282
313
  })
283
314
  })
284
315
 
316
+ it('writes edgeToEdgeEnabled to gradle.properties', async () => {
317
+ const projectRoot = expoProject()
318
+ const config = withVxrn({
319
+ name: 'TestApp',
320
+ slug: 'test-app',
321
+ _internal: { projectRoot },
322
+ })
323
+ const properties = (
324
+ await config.mods.android.gradleProperties({
325
+ ...config,
326
+ modRequest: { projectRoot, introspect: false, projectName: 'TestApp' },
327
+ modResults: [{ type: 'property', key: 'hermesEnabled', value: 'true' }],
328
+ })
329
+ ).modResults
330
+ expect(properties).toContainEqual({
331
+ type: 'property',
332
+ key: 'edgeToEdgeEnabled',
333
+ value: 'true',
334
+ })
335
+ })
336
+
285
337
  it('resolves Expo config plugins from the app and binds the dangerous mod', async () => {
286
338
  const projectRoot = expoProject()
287
339
  const iosRoot = join(projectRoot, 'ios')
@@ -820,6 +820,24 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autoli
820
820
  )
821
821
  })
822
822
 
823
+ it('writes edgeToEdgeEnabled to gradle.properties', () => {
824
+ const props = renderPrebuildFile({
825
+ relativePath: 'gradle.properties',
826
+ content: 'hermesEnabled=true',
827
+ platform: 'android',
828
+ app,
829
+ })
830
+ expect(props.content).toContain('edgeToEdgeEnabled=true')
831
+
832
+ const twice = renderPrebuildFile({
833
+ relativePath: 'gradle.properties',
834
+ content: props.content,
835
+ platform: 'android',
836
+ app,
837
+ })
838
+ expect(twice.content).toBe(props.content)
839
+ })
840
+
823
841
  it('omits notification entries when notifications is unset', () => {
824
842
  const bare = { ...app, notifications: undefined }
825
843
  const infoPlist = renderPrebuildFile({
@@ -69,6 +69,7 @@ type NativeProjectPatches = {
69
69
  replaceAppBuildGradleReactBlock(input: string): string
70
70
  addDepsPatchToAppBuildGradle(input: string): string
71
71
  addReactNativeScreensFix(input: string): string
72
+ enableEdgeToEdgeInMainActivity(mainActivity: string): string
72
73
  }
73
74
 
74
75
  const requireVxrn = module.createRequire(import.meta.url)
@@ -2511,6 +2512,17 @@ ${schemes.map((scheme) => `\t\t\t\t<string>${scheme}</string>`).join('\n')}
2511
2512
  rendered = `${trailed}\n# One.UI.Map: set by native.app.android.googleMapsApiKey.\n${line}\n`
2512
2513
  }
2513
2514
  }
2515
+ if (platform === 'android' && relativePath === 'gradle.properties') {
2516
+ // MainActivity opts into androidx edge-to-edge; this tells react
2517
+ // native the window is edge-to-edge on every api level, so its
2518
+ // status bar paths skip the legacy decorFitsSystemWindows route that
2519
+ // would undo it on android 14 and older.
2520
+ const line = 'edgeToEdgeEnabled=true'
2521
+ if (!rendered.includes(line)) {
2522
+ const trailed = rendered.endsWith('\n') ? rendered : `${rendered}\n`
2523
+ rendered = `${trailed}\n# Edge-to-edge: set by one prebuild to match enableEdgeToEdge in MainActivity.\n${line}\n`
2524
+ }
2525
+ }
2514
2526
  if (
2515
2527
  platform === 'android' &&
2516
2528
  relativePath === 'app/src/main/res/values/styles.xml' &&
@@ -3250,11 +3262,16 @@ export function applyAndroidDependencyPatches(args: {
3250
3262
  'MainActivity.kt'
3251
3263
  )
3252
3264
  const rendered = nativeProjectPatches.holdLaunchScreenInMainActivity(
3253
- nativeProjectPatches.addReactNativeScreensFix(readFileSync(activityPath, 'utf8'))
3265
+ nativeProjectPatches.enableEdgeToEdgeInMainActivity(
3266
+ nativeProjectPatches.addReactNativeScreensFix(readFileSync(activityPath, 'utf8'))
3267
+ )
3254
3268
  )
3255
3269
  if (!rendered.includes('RNScreensFragmentFactory')) {
3256
3270
  throw new Error('[vxrn] failed to apply the react-native-screens activity patch')
3257
3271
  }
3272
+ if (!rendered.includes('enableEdgeToEdge')) {
3273
+ throw new Error('[vxrn] failed to apply the edge-to-edge activity patch')
3274
+ }
3258
3275
  writeFileSync(activityPath, rendered)
3259
3276
  }
3260
3277
 
@@ -132,7 +132,9 @@ fun helper(x: Int) = x`
132
132
  ])
133
133
  const glue = renderKotlinSourceGlue('K_counter', contract).source
134
134
  expect(glue).toContain('class OneNativeSourceViews_K_counter: OneNativeSourceViewDispatch')
135
- expect(glue).toContain('onPick = if (props.optBoolean("onPick")) ({ a0 -> emit("onPick"')
135
+ expect(glue).toContain('val reader = OneJsonReader_K_counter(propsJson)')
136
+ expect(glue).not.toContain('JSONObject(propsJson)')
137
+ expect(glue).toContain('onPick = if (h.hasOnPick) ({ a0 -> emit("onPick"')
136
138
  expect(() =>
137
139
  nativeSourceContract('/app/native/Bad.kt', '@Composable\nfun Bad(count: Int = 1) {}')
138
140
  ).toThrow(/Bad\.kt:2:.*may only default to null/)
@@ -141,6 +143,51 @@ fun helper(x: Int) = x`
141
143
  ).toThrow(/must return Unit/)
142
144
  })
143
145
 
146
+ it('decodes keyed change documents into held view state', () => {
147
+ const contract = nativeSourceContract(
148
+ '/app/native/Counter.kt',
149
+ `package app.counter
150
+ data class Row(val id: Int, val title: String)
151
+ @Composable
152
+ fun Counter(rows: List<Row>, tick: Int, note: String? = null, onTap: () -> Unit, onPick: ((Int) -> Unit)? = null, modifier: Modifier = Modifier) {
153
+ Text("x")
154
+ }`
155
+ )
156
+ const glue = renderKotlinSourceGlue('K_counter', contract).source
157
+ // decode runs at prop commit, render reads the held props: a change whose
158
+ // base the host no longer holds refuses (false) instead of rendering stale
159
+ expect(glue).toContain('fun apply(view: String, propsJson: String, state: OneNativeSourceKeyedState): Boolean')
160
+ expect(glue).toContain('fun Content(view: String, state: OneNativeSourceKeyedState, emit: (String, String) -> Unit)')
161
+ expect(glue).toContain('private class HeldCounter_K_counter {')
162
+ expect(glue).toContain('var rows: List<app.counter.Row>? = null')
163
+ expect(glue).toContain('var tick: Int = 0')
164
+ expect(glue).toContain('var hasOnPick: Boolean = false')
165
+ expect(glue).toContain('reader.selectName("\\$seq", "\\$base", "\\$lists", "rows", "tick", "note", "onTap", "onPick")')
166
+ expect(glue).toContain('if (hasBase && !hasSeq) error("seq: required")')
167
+ expect(glue).toContain('if (sawLists && !hasBase) error("lists: needs a change document")')
168
+ expect(glue).toContain('state.views["Counter"]')
169
+ expect(glue).toContain('state.seq = seq')
170
+ expect(glue).toContain('if (seq == state.seq) return true')
171
+ expect(glue).toContain('return false')
172
+ expect(glue).toContain('state.revision++')
173
+ expect(glue).toContain('val revision = state.revision')
174
+ expect(glue).toContain('private fun mergeListsCounterRows(reader: OneJsonReader_K_counter, old: List<app.counter.Row>?, prop: String): List<app.counter.Row>? {')
175
+ expect(glue).toContain('val current = old ?: return null')
176
+ expect(glue).toContain('var rowsMergeFailed = false')
177
+ expect(glue).toContain('if (mergedRows == null) rowsMergeFailed = true else rows = mergedRows')
178
+ expect(glue).toContain('if (rowsMergeFailed) error("lists.rows: cannot merge")')
179
+ expect(glue).toContain('lists.$prop: needs length')
180
+ expect(glue).toContain('lists.$prop: set out of order')
181
+ expect(glue).not.toContain('no held list')
182
+ expect(glue).not.toContain('missing index')
183
+ expect(glue).toContain('val si = readInt(itemReader, itemPath); if (!itemReader.hasNext()) error("$itemPath: expected element")')
184
+ expect(glue).toContain('rows = h.rows!!')
185
+ // apply owns the one reader and closes the document it decoded; a second
186
+ // close throws on device (expected '}' at end of document).
187
+ expect(glue.match(/val reader = OneJsonReader_K_counter\(propsJson\)/g)).toHaveLength(1)
188
+ expect(glue.match(/reader\.endDocument\(\)/g)).toHaveLength(1)
189
+ })
190
+
144
191
  it('omits the events import and date/data helpers a contract never uses', () => {
145
192
  // kotlin/wasm cannot resolve the jvm/android/event-host symbols, so a
146
193
  // resources-shaped contract must not reference them.
@@ -167,6 +214,7 @@ fun Resources(title: String, onStep: ((Int) -> Unit)? = null) {
167
214
  expect(glue).not.toContain(symbol)
168
215
  }
169
216
  expect(glue).toContain('import androidx.compose.runtime.Composable')
217
+ expect(glue).toContain('import dev.onejs.one.OneNativeSourceKeyedState')
170
218
  expect(glue).toContain('import dev.onejs.one.OneNativeSourceDispatch')
171
219
  expect(glue).toContain('title = readString(')
172
220
  const dated = nativeSourceContract(
@@ -213,8 +261,12 @@ struct Helper: View {
213
261
  ])
214
262
  const glue = renderSwiftSourceGlue('rn_host_app', [contract]).source
215
263
  expect(glue).toContain('@objc(OneNativeSourceViews_rn_host_app) @MainActor public final class OneNativeSourceViews_rn_host_app: NSObject, OneNativeSourceViewDispatch')
216
- expect(glue).toContain('value: try decode(props["value"] ?? missing("props.value"), as: Double.self),')
217
- expect(glue).toContain('onReset: (props["onReset"] as? Bool) == true ? { () -> Void in emit("onReset", args([])) } : nil')
264
+ expect(glue).toContain('var `value`: Double')
265
+ expect(glue).toContain('private struct Doc_Level: Decodable')
266
+ expect(glue).toContain('guard let props = held as? Held_Level else')
267
+ expect(glue).toContain('public func updateView(_ held: AnyObject?, _ name: String, _ document: String) throws -> AnyObject?')
268
+ expect(glue).toContain('value: props.`value`,')
269
+ expect(glue).toContain('onReset: props.`onReset` == true ? { () -> Void in emit("onReset", args([])) } : nil')
218
270
 
219
271
  const temp = mkdtempSync(join(tmpdir(), 'one-native-source-views-'))
220
272
  writeFileSync(join(temp, 'Level.d.swift.ts'), contract.declaration)
@@ -222,10 +274,10 @@ struct Helper: View {
222
274
  compilerOptions: { strict: true, noEmit: true, allowArbitraryExtensions: true, module: 'esnext', moduleResolution: 'bundler', types: [] },
223
275
  include: ['use.ts'],
224
276
  }))
225
- writeFileSync(join(temp, 'react.d.ts'), "declare module 'react' { export interface ReactElement {} }")
277
+ writeFileSync(join(temp, 'react.d.ts'), "declare module 'react' { export interface ReactElement {} }\ndeclare module 'react-native' { export type ViewStyle = { flex?: number }; export type StyleProp<T> = T | null | undefined }")
226
278
  writeFileSync(join(temp, 'use.ts'), `/// <reference path="./react.d.ts" />
227
279
  import { Level } from './Level.swift'
228
- Level({ value: 1, onChange: (next: number) => void next, onReset: null })
280
+ Level({ value: 1, onChange: (next: number) => void next, onReset: null, style: { flex: 1 } })
229
281
  Level({ value: 'wrong', onChange: () => {} })
230
282
  `)
231
283
  // one compilation must accept the valid element and reject only the wrong prop.
@@ -511,7 +563,18 @@ object Atlas : OneModule {
511
563
  'object Atlas : OneModule {\n fun at(moment: java.util.Date): java.util.Date = moment\n}'
512
564
  )
513
565
  expect(kotlin.modules[0].methods[0].parameters[0].type).toBe('Date')
514
- expect(renderKotlinSourceGlue('K_atlas', kotlin).source).toContain('readDate(args.get(0), "args[0]")')
566
+ expect(renderKotlinSourceGlue('K_atlas', kotlin).source).toContain('readDate(reader, "args[0]")')
567
+ })
568
+
569
+ it('eats commas between positional module args', () => {
570
+ const kotlin = nativeSourceContract(
571
+ '/app/native/Atlas.kt',
572
+ 'object Atlas : OneModule {\n fun combine(first: Int, second: String): String = second\n}'
573
+ )
574
+ const glue = renderKotlinSourceGlue('K_atlas', kotlin).source
575
+ expect(glue).toContain('val arg0 = readInt(reader, "args[0]")')
576
+ expect(glue).toContain('require(reader.hasNext()) { "K_atlas.Atlas.combine expects 2 arguments" }')
577
+ expect(glue).toContain('val arg1 = readString(reader, "args[1]")')
515
578
  })
516
579
 
517
580
  it('rejects unusable named types at their use site', () => {
@@ -573,10 +636,12 @@ public struct Level: View {
573
636
 
574
637
  const kotlin = nativeSourceContract('/app/native/Atlas.kt', kotlinSource)
575
638
  const kotlinGlue = renderKotlinSourceGlue('K_atlas', kotlin).source
576
- expect(kotlinGlue).toContain('private fun readPoint(value: Any, path: String): app.atlas.Point {')
639
+ expect(kotlinGlue).toContain('private class OneJsonReader_K_atlas(private val text: String) {')
640
+ expect(kotlinGlue).toContain('private fun readPoint(reader: OneJsonReader_K_atlas, path: String): app.atlas.Point {')
577
641
  expect(kotlinGlue).toContain('private fun encodePoint(value: app.atlas.Point): JSONObject {')
578
- expect(kotlinGlue).toContain('private fun readDate(value: Any, path: String): java.util.Date {')
579
- expect(kotlinGlue).toContain('readPoint(args.get(0), "args[0]")')
642
+ expect(kotlinGlue).toContain('private fun readDate(reader: OneJsonReader_K_atlas, path: String): java.util.Date {')
643
+ expect(kotlinGlue).toContain('readPoint(reader, "args[0]")')
644
+ expect(kotlinGlue).not.toContain('JSONArray(argsJson)')
580
645
  expect(kotlinGlue).toContain('JSONArray().put(dateToString(result))')
581
646
  })
582
647