@bun-native/react 0.0.0-bootstrap.0 → 0.33.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bunly contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @bun-native/react
2
+
3
+ React renderer for Bun Native desktop windows.
4
+
5
+ Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.0 was built from [fa8bef754122](https://github.com/0xPlayerOne/bun-native/tree/fa8bef754122621d4235eb06e30bb1085b4f7b1e) and published from GitHub Actions with npm provenance. Use the Bun revision pinned in that commit's toolchain.json. Start with the [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/fa8bef754122621d4235eb06e30bb1085b4f7b1e/docs/getting-started.md); `bun-native host install` fetches the matching release host. The license notice is in LICENSE.
@@ -0,0 +1,59 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "commit": "fa8bef754122621d4235eb06e30bb1085b4f7b1e",
4
+ "tree": "f805e2eb19a4e10fc29d1dc827cf5bc617b5773b",
5
+ "files": {
6
+ "package.json": {
7
+ "bytes": 2164,
8
+ "sha256": "db50b6f81f25c113a46f8c9b038bc548a92effaeb4ad9562e5e95044baa7b66f"
9
+ },
10
+ "packages/react/package.json": {
11
+ "bytes": 402,
12
+ "sha256": "18ac27eaf168a16c333e2b79e9aa7fa9ff9caf272fbf27523d865848946503ec"
13
+ },
14
+ "packages/react/src/commit-barrier.ts": {
15
+ "bytes": 987,
16
+ "sha256": "139f9f2827d3363642fee801e128dd1abf2a754d04550aca7d878493ef19bfd0"
17
+ },
18
+ "packages/react/src/index.ts": {
19
+ "bytes": 338,
20
+ "sha256": "211d06eb159c3afa4ceac566538e7cbc7ad4d106a4c58d05d7bb0cd842e2acda"
21
+ },
22
+ "packages/react/src/jsx-dev-runtime.ts": {
23
+ "bytes": 93,
24
+ "sha256": "bc127e1290abc7a6ba83fac0647b9f8ecca35c7aaa025a9ad25e35289a93bd85"
25
+ },
26
+ "packages/react/src/jsx-runtime.ts": {
27
+ "bytes": 92,
28
+ "sha256": "df1c78b5c254c4ccfa226d9b3d0c2b2a9dbf0733d40deac4f45cb47692a3763d"
29
+ },
30
+ "packages/react/src/jsx.ts": {
31
+ "bytes": 3189,
32
+ "sha256": "4458ba8196591d768d0c7d1a333e47fb2902f44b3e848b9fd7289f68092b8502"
33
+ },
34
+ "packages/react/src/mount.ts": {
35
+ "bytes": 7530,
36
+ "sha256": "73b271cb21be8774674a428550cd3cab42c065b6b8286f7c1c914e86fe65addd"
37
+ },
38
+ "packages/react/src/reconciler.ts": {
39
+ "bytes": 9500,
40
+ "sha256": "f29e9099e177009760406e4164bf8b740715d89bf6bc12b450cb4d575880615a"
41
+ },
42
+ "packages/react/src/root-errors.ts": {
43
+ "bytes": 963,
44
+ "sha256": "2380b9bcc709010f9f09bef444fda47fdda9f64f1fd22d0a4b351069ce09826a"
45
+ },
46
+ "LICENSE": {
47
+ "bytes": 1075,
48
+ "sha256": "86673fb485f12fa9aaa3941dfccc9f69a7f8322f8141d0d5c73db4323de10ac5"
49
+ }
50
+ },
51
+ "toolchain": {
52
+ "bytes": 3012,
53
+ "sha256": "00c3f707aae8abe9ca75cd2e162f5e1c7c34362a03675da39afead60d856066a"
54
+ },
55
+ "dependencyLock": {
56
+ "bytes": 28357,
57
+ "sha256": "65e3dbd01be4a1889367690faf1a9c3f08fb31d22dbd273d9bb9f39dd6baa6fb"
58
+ }
59
+ }
package/package.json CHANGED
@@ -1 +1,40 @@
1
- {"name":"@bun-native/react","version":"0.0.0-bootstrap.0","description":"Placeholder. Releases publish from GitHub Actions.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/0xPlayerOne/bun-native.git"}}
1
+ {
2
+ "name": "@bun-native/react",
3
+ "version": "0.33.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "files": [
7
+ "src",
8
+ "LICENSE",
9
+ "README.md",
10
+ "bun-native-source.json"
11
+ ],
12
+ "engines": {
13
+ "bun": "1.4.4-canary.1"
14
+ },
15
+ "exports": {
16
+ ".": "./src/index.ts",
17
+ "./jsx-runtime": "./src/jsx-runtime.ts",
18
+ "./jsx-dev-runtime": "./src/jsx-dev-runtime.ts"
19
+ },
20
+ "description": "React renderer for Bun Native desktop windows.",
21
+ "dependencies": {
22
+ "@bun-native/core": "0.33.0",
23
+ "@bun-native/renderer": "0.33.0",
24
+ "react": "19.3.1",
25
+ "react-reconciler": "0.34.0",
26
+ "@types/react": "19.3.0",
27
+ "@types/react-reconciler": "0.34.1"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/0xPlayerOne/bun-native.git",
32
+ "directory": "packages/react"
33
+ },
34
+ "homepage": "https://github.com/0xPlayerOne/bun-native#readme",
35
+ "bugs": "https://github.com/0xPlayerOne/bun-native/issues",
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ }
40
+ }
@@ -0,0 +1,31 @@
1
+ /** Revocable ownership for a callback retained by an opaque reconciler update. */
2
+ export interface CommitBarrierBindings {
3
+ release: (() => void) | undefined
4
+ resolve: (() => void) | undefined
5
+ reject: ((cause: unknown) => void) | undefined
6
+ }
7
+
8
+ function settle(bindings: CommitBarrierBindings, failed: boolean, cause?: unknown): void {
9
+ const release = bindings.release
10
+ const resolve = bindings.resolve
11
+ const reject = bindings.reject
12
+ // Revoke the actual root/promise bindings before callbacks can reenter.
13
+ bindings.release = undefined
14
+ bindings.resolve = undefined
15
+ bindings.reject = undefined
16
+ release?.()
17
+ if (failed) reject?.(cause)
18
+ else resolve?.()
19
+ }
20
+
21
+ /** Keep this factory outside Root's scope; retained callbacks own only this cell. */
22
+ export function createCommitBarrier(bindings: CommitBarrierBindings) {
23
+ return {
24
+ complete(): void {
25
+ settle(bindings, false)
26
+ },
27
+ reject(cause: unknown): void {
28
+ settle(bindings, true, cause)
29
+ },
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export {
2
+ focusNode,
3
+ inputState,
4
+ registerDropTarget,
5
+ scrollNodeIntoView,
6
+ scrollNodeTo,
7
+ } from '@bun-native/renderer'
8
+ export type { RendererNode as NativeRef } from '@bun-native/renderer'
9
+ export { render } from './mount.ts'
10
+ export type { ReactRenderOptions, ReactRenderRoot } from './mount.ts'
11
+ export type { JSX } from './jsx.ts'
@@ -0,0 +1,2 @@
1
+ export { Fragment, jsxDEV } from 'react/jsx-dev-runtime'
2
+ export type { JSX } from './jsx.ts'
@@ -0,0 +1,2 @@
1
+ export { Fragment, jsx, jsxs } from 'react/jsx-runtime'
2
+ export type { JSX } from './jsx.ts'
package/src/jsx.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { Element as NativeElement, InputState } from '@bun-native/core'
2
+ import type { EventHandlers, RendererNode } from '@bun-native/renderer'
3
+ import type { JSX as ReactJSX, ReactNode, Ref } from 'react'
4
+
5
+ export namespace JSX {
6
+ export type Element = ReactJSX.Element
7
+ export type ElementType = ReactJSX.ElementType
8
+ export type ElementClass = ReactJSX.ElementClass
9
+ export type ElementAttributesProperty = ReactJSX.ElementAttributesProperty
10
+ export type ElementChildrenAttribute = ReactJSX.ElementChildrenAttribute
11
+ export type LibraryManagedAttributes<C, P> = ReactJSX.LibraryManagedAttributes<C, P>
12
+ export type IntrinsicAttributes = ReactJSX.IntrinsicAttributes
13
+ export type IntrinsicClassAttributes<T> = ReactJSX.IntrinsicClassAttributes<T>
14
+
15
+ type Base<K extends NativeElement['kind']> = EventHandlers &
16
+ ReactJSX.IntrinsicAttributes & {
17
+ style?: NonNullable<Extract<NativeElement, { kind: K }>['style']>
18
+ ref?: Ref<RendererNode>
19
+ }
20
+ type TextContent = { text?: string; children?: ReactNode }
21
+ export interface IntrinsicElements {
22
+ /**
23
+ * Requires `accessibility_lists_v1` for the accessible members. A list's
24
+ * `accessibleSetSize` counts every item, mounted or not; an item's
25
+ * `accessiblePosition` is its 1-based place in that full set.
26
+ */
27
+ box: Base<'box'> & { children?: ReactNode } & {
28
+ accessibleRole?: 'list' | 'list_item'
29
+ /** Only with `accessibleRole`. */
30
+ accessibleLabel?: string
31
+ /** Only on a `list`. */
32
+ accessibleSetSize?: number
33
+ /** Only on a `list_item`; 1-based. */
34
+ accessiblePosition?: number
35
+ /** Only on a `list_item`. */
36
+ accessibleSelected?: boolean
37
+ }
38
+ text: Base<'text'> &
39
+ TextContent & {
40
+ /** Requires `accessibility_lists_v1`: announce the text when it appears and changes. */
41
+ accessibleLive?: 'polite' | 'assertive'
42
+ }
43
+ button: Base<'button'> & TextContent & { disabled?: boolean }
44
+ input: Base<'input'> & {
45
+ value?: string | InputState
46
+ placeholder?: string
47
+ /** Requires the host's `input_states_v1` capability. */
48
+ disabled?: boolean
49
+ /** Requires the host's `input_states_v1` capability. */
50
+ readOnly?: boolean
51
+ }
52
+ /** At most two pane children; the host owns the divider drag and keyboard steps. */
53
+ split: Base<'split'> & {
54
+ children?: ReactNode
55
+ ratio?: number
56
+ min_size?: number
57
+ /** Requires `accessibility_v1`: the divider's accessible name. */
58
+ accessibleLabel?: string
59
+ }
60
+ /** Painted above the ordinary tree; only window-anchored overlays may be modal. */
61
+ overlay: Base<'overlay'> & {
62
+ children?: ReactNode
63
+ /** Requires `overlay_keyboard_v1`: focus the first enabled control on open. */
64
+ autofocus?: boolean
65
+ /** Requires `overlay_keyboard_v1`: `menu` adds arrow-key roving focus and type-ahead. */
66
+ navigation?: 'tab' | 'menu'
67
+ /** Requires `accessibility_v1`: the dialog's accessible name. */
68
+ accessibleLabel?: string
69
+ } & (
70
+ | { anchor?: 'window'; modal?: boolean; surface?: 'layer' }
71
+ | { anchor: 'parent'; modal?: false; surface?: 'layer' | 'window' }
72
+ )
73
+ }
74
+ }
package/src/mount.ts ADDED
@@ -0,0 +1,197 @@
1
+ import { NativeStateError, type NativeWindow, type Style } from '@bun-native/core'
2
+ import { createRenderRoot, type RenderOptions, type RenderRoot } from '@bun-native/renderer'
3
+ import type { ErrorInfo, ReactNode } from 'react'
4
+ import { ConcurrentRoot } from 'react-reconciler/constants'
5
+ import { reconciler } from './reconciler.ts'
6
+ import { createRootErrors } from './root-errors.ts'
7
+ import { createCommitBarrier } from './commit-barrier.ts'
8
+
9
+ type FiberRoot = ReturnType<typeof reconciler.createContainer>
10
+
11
+ export interface ReactRenderRoot extends RenderRoot {
12
+ /** Submit a new React tree; resolves after its native acknowledgement. */
13
+ render(children: ReactNode): Promise<void>
14
+ }
15
+
16
+ export interface ReactRenderOptions extends RenderOptions {
17
+ /** Synchronously observe errors React recovered from; defaults to console.error. A thrown reporter error terminates this root. */
18
+ onRecoverableError?: (error: unknown, info: Readonly<ErrorInfo>) => void
19
+ }
20
+
21
+ class Root implements ReactRenderRoot {
22
+ readonly ready: Promise<void>
23
+ readonly closed: Promise<void>
24
+ private readonly native: RenderRoot
25
+ private fiber!: FiberRoot
26
+ private errors: ReturnType<typeof createRootErrors> | undefined
27
+ private element: ReactNode
28
+ private phase: 'active' | 'disposing' | 'closed' = 'active'
29
+ private failure: Error | undefined
30
+ private disposal: Promise<void> | undefined
31
+ private readonly barriers = new Set<ReturnType<typeof createCommitBarrier>>()
32
+
33
+ constructor(
34
+ children: ReactNode,
35
+ window: NativeWindow,
36
+ onRecoverableError: NonNullable<ReactRenderOptions['onRecoverableError']>,
37
+ style: Style | undefined
38
+ ) {
39
+ this.element = children
40
+ this.native = createRenderRoot(
41
+ window,
42
+ ({ container, onCleanup, fail }) => {
43
+ const terminate = (error: unknown) => {
44
+ this.failure ??= error instanceof Error ? error : new Error(String(error))
45
+ // React invokes this inside its commit. Teardown must start after that
46
+ // stack returns, rather than recursively entering Fiber's commit.
47
+ queueMicrotask(() => fail(this.failure))
48
+ }
49
+ this.errors = createRootErrors({ terminate, report: onRecoverableError })
50
+ this.fiber = reconciler.createContainer(
51
+ container,
52
+ ConcurrentRoot,
53
+ null,
54
+ false,
55
+ null,
56
+ '',
57
+ this.errors.uncaught,
58
+ this.errors.caught,
59
+ this.errors.recoverable,
60
+ this.errors.transition,
61
+ null
62
+ )
63
+ onCleanup(() => {
64
+ this.phase = 'disposing'
65
+ try {
66
+ reconciler.updateContainerSync(null, this.fiber, null)
67
+ reconciler.flushSyncWork()
68
+ reconciler.flushPassiveEffects()
69
+ // React reports unmount effect/ref errors through onUncaughtError.
70
+ // The shared root is already disposing, so forward the synchronous
71
+ // result here instead of relying on a later terminal-error request.
72
+ if (this.failure) throw this.failure
73
+ } finally {
74
+ // Framework teardown is terminal even while native removal awaits
75
+ // acknowledgement. Keep unmount errors active until this boundary.
76
+ this.errors?.release()
77
+ this.errors = undefined
78
+ }
79
+ })
80
+ reconciler.updateContainerSync(children, this.fiber, null)
81
+ reconciler.flushSyncWork()
82
+ if (this.failure !== undefined) throw this.failure
83
+ reconciler.flushPassiveEffects()
84
+ },
85
+ style === undefined ? {} : { style }
86
+ )
87
+ this.ready = this.native.ready.then(() => {
88
+ if (this.phase !== 'active')
89
+ throw new NativeStateError(
90
+ 'invalid_handle',
91
+ 'React root closed before initial native acknowledgement'
92
+ )
93
+ })
94
+ void this.ready.catch(() => {})
95
+ this.closed = this.native.closed
96
+ void this.closed.then(
97
+ () => this.finish(),
98
+ (error) => {
99
+ this.failure = error
100
+ this.finish()
101
+ }
102
+ )
103
+ }
104
+
105
+ get diagnostics() {
106
+ return this.native.diagnostics
107
+ }
108
+
109
+ private finish(): void {
110
+ this.phase = 'closed'
111
+ this.errors?.release()
112
+ this.errors = undefined
113
+ const error = this.failure ?? new NativeStateError('invalid_state', 'React root is closed')
114
+ for (const barrier of this.barriers) barrier.reject(error)
115
+ this.barriers.clear()
116
+ this.element = null
117
+ this.fiber = null
118
+ }
119
+
120
+ private assertActive(): void {
121
+ if (this.failure !== undefined) throw this.failure
122
+ if (this.phase !== 'active') throw new NativeStateError('invalid_state', 'React root is closed')
123
+ }
124
+
125
+ async render(children: ReactNode): Promise<void> {
126
+ this.assertActive()
127
+ this.element = children
128
+ reconciler.updateContainerSync(children, this.fiber, null)
129
+ reconciler.flushSyncWork()
130
+ await this.flush()
131
+ }
132
+
133
+ async flush(): Promise<void> {
134
+ this.assertActive()
135
+ // A normal-priority root update is a Fiber commit barrier for already
136
+ // scheduled ordinary hook updates. It does not wait for future async work,
137
+ // unresolved Suspense, or lower-priority transitions.
138
+ reconciler.flushPassiveEffects()
139
+ await new Promise<void>((resolve, reject) => {
140
+ if (this.barriers.size >= 128) {
141
+ reject(new NativeStateError('limit_exceeded', 'React commit barrier queue is full'))
142
+ return
143
+ }
144
+ const barrier = createCommitBarrier({
145
+ release: () => this.barriers.delete(barrier),
146
+ resolve,
147
+ reject,
148
+ })
149
+ this.barriers.add(barrier)
150
+ try {
151
+ reconciler.updateContainer(this.element, this.fiber, null, barrier.complete)
152
+ } catch (cause) {
153
+ // A rejected submission owns no future commit. Release its capacity and
154
+ // callback bindings now instead of poisoning all later flushes.
155
+ barrier.reject(cause)
156
+ }
157
+ })
158
+ // Native failure cleanup may already have entered disposal. Join it before
159
+ // checking local lifetime so the triggering operation preserves its cause.
160
+ await this.native.flush()
161
+ this.assertActive()
162
+ // Effect code may schedule future work; its mere execution is not evidence
163
+ // that another render is pending. Re-enqueuing barriers until this returns
164
+ // false would itself keep producing commits indefinitely.
165
+ reconciler.flushPassiveEffects()
166
+ await this.native.flush()
167
+ }
168
+
169
+ dispose(): Promise<void> {
170
+ if (!this.disposal) {
171
+ this.phase = 'disposing'
172
+ // A listener/layout effect may request disposal. Never reenter Fiber's
173
+ // render or commit from the current application callback stack.
174
+ this.disposal = reconciler.isAlreadyRendering()
175
+ ? Promise.resolve().then(() => this.native.dispose())
176
+ : this.native.dispose()
177
+ }
178
+ return this.disposal
179
+ }
180
+ }
181
+
182
+ /** Mount React into one independently disposable shared native subtree. */
183
+ export function render(
184
+ children: ReactNode,
185
+ window: NativeWindow,
186
+ options: ReactRenderOptions = {}
187
+ ): ReactRenderRoot {
188
+ if (!options || typeof options !== 'object' || Array.isArray(options))
189
+ throw new TypeError('React render options must be an object')
190
+ for (const name of Object.keys(options))
191
+ if (name !== 'onRecoverableError' && name !== 'style')
192
+ throw new TypeError('Unknown React render option: ' + name)
193
+ const reporter = options.onRecoverableError
194
+ if (reporter !== undefined && typeof reporter !== 'function')
195
+ throw new TypeError('onRecoverableError must be a function')
196
+ return new Root(children, window, reporter ?? console.error, options.style)
197
+ }
@@ -0,0 +1,258 @@
1
+ import { UnsupportedFeatureError, type Listener, type NativeUIEvent } from '@bun-native/core'
2
+ import {
3
+ createElement,
4
+ createTextNode,
5
+ insertNode,
6
+ isEventProperty,
7
+ removeNode,
8
+ replaceText,
9
+ setProperty,
10
+ type ElementTag,
11
+ type EventProperty,
12
+ type Node,
13
+ type PropertyName,
14
+ type RendererNode,
15
+ } from '@bun-native/renderer'
16
+ import { createContext } from 'react'
17
+ import Reconciler from 'react-reconciler'
18
+ import { DefaultEventPriority, NoEventPriority } from 'react-reconciler/constants'
19
+
20
+ type Props = Readonly<Record<string, unknown>>
21
+ type HostContext = Readonly<Record<string, never>>
22
+ type Timer = ReturnType<typeof setTimeout>
23
+
24
+ // Unsupported host surfaces use never; no React Fiber fields are inspected.
25
+ type Config = Reconciler.HostConfig<
26
+ string,
27
+ Props,
28
+ Node,
29
+ Node,
30
+ Node,
31
+ never,
32
+ never,
33
+ never,
34
+ never,
35
+ RendererNode,
36
+ HostContext,
37
+ never,
38
+ Timer,
39
+ null,
40
+ null,
41
+ null,
42
+ never,
43
+ never,
44
+ never,
45
+ never
46
+ >
47
+
48
+ const context: HostContext = Object.freeze({})
49
+ let updatePriority: Reconciler.EventPriority = NoEventPriority
50
+ let activeEvent: { type: string; observedAt: number } | undefined
51
+ type EventBinding = {
52
+ listener: Listener<NativeUIEvent> | undefined
53
+ dispatch: Listener<NativeUIEvent>
54
+ }
55
+ const bindings = new WeakMap<Node, Map<EventProperty, EventBinding>>()
56
+
57
+ function unsupported(feature: string): never {
58
+ throw new UnsupportedFeatureError('react-' + feature)
59
+ }
60
+
61
+ // React requires these hooks even when a host has no corresponding resource.
62
+ // Reuse their identical semantics instead of maintaining separate empty bodies.
63
+ const noOp = (): void => {}
64
+ const noInstance = () => null
65
+ const returnFalse = () => false
66
+ const hostContext = () => context
67
+
68
+ function createBinding(listener: Listener<NativeUIEvent>): EventBinding {
69
+ const binding: EventBinding = { listener, dispatch }
70
+ function dispatch(event: NativeUIEvent): void | Promise<void> {
71
+ const current = binding.listener
72
+ if (!current) return
73
+ const previous = activeEvent
74
+ activeEvent = { type: event.type, observedAt: performance.now() }
75
+ try {
76
+ return reconciler.discreteUpdates(current, event, undefined, undefined, undefined)
77
+ } finally {
78
+ activeEvent = previous
79
+ reconciler.flushSyncWork()
80
+ }
81
+ }
82
+ return binding
83
+ }
84
+
85
+ function updateEvent(node: Node, name: EventProperty, value: unknown): void {
86
+ const registered = bindings.get(node)
87
+ const existing = registered?.get(name)
88
+ if (value === undefined) {
89
+ if (existing) existing.listener = undefined
90
+ registered?.delete(name)
91
+ setProperty(node, name, undefined)
92
+ return
93
+ }
94
+ if (typeof value !== 'function') throw new TypeError('Native event listeners must be functions')
95
+ // The callable boundary is checked here; core supplies the validated event.
96
+ const listener = value as Listener<NativeUIEvent>
97
+ if (existing) {
98
+ // React replaces inline closures on ordinary rerenders. Keep the node's
99
+ // native type/phase registration alive so queued events retain their ordering.
100
+ existing.listener = listener
101
+ return
102
+ }
103
+ const binding = createBinding(listener)
104
+ setProperty(node, name, binding.dispatch)
105
+ const next = registered ?? new Map<EventProperty, EventBinding>()
106
+ next.set(name, binding)
107
+ bindings.set(node, next)
108
+ }
109
+
110
+ function updateProperty(node: Node, name: string, value: unknown): void {
111
+ if (isEventProperty(name)) updateEvent(node, name, value)
112
+ // React props arrive as strings; the renderer rejects unknown names at runtime.
113
+ else setProperty(node, name as PropertyName, value)
114
+ }
115
+
116
+ const reserved = (name: string) => name === 'children' || name === 'ref' || name === 'key'
117
+
118
+ type StyleObject = Readonly<Record<string, unknown>>
119
+ const isStyleObject = (value: unknown): value is StyleObject =>
120
+ typeof value === 'object' && value !== null && !Array.isArray(value)
121
+
122
+ /**
123
+ * Inline `style={{ ... }}` is a new object on every render. The renderer keeps
124
+ * a shallow copy, so an object with the same own keys, in the same order, and
125
+ * the same values commits the identical element and sends no native set;
126
+ * setting it anyway would only recapture the node and its ancestors.
127
+ */
128
+ function sameStyle(previous: unknown, next: unknown): boolean {
129
+ if (!isStyleObject(previous) || !isStyleObject(next)) return false
130
+ const names = Object.keys(previous)
131
+ const nextNames = Object.keys(next)
132
+ if (names.length !== nextNames.length) return false
133
+ for (let index = 0; index < names.length; index += 1) {
134
+ const name = names[index]!
135
+ if (name !== nextNames[index] || !Object.is(previous[name], next[name])) return false
136
+ }
137
+ return true
138
+ }
139
+
140
+ // React commits every rerendered host fiber, so this runs once per row of a
141
+ // rerendered list: compare in place rather than allocating a merged key set.
142
+ function updateProperties(node: Node, previous: Props, next: Props): void {
143
+ for (const name of Object.keys(previous))
144
+ if (!reserved(name) && !Object.hasOwn(next, name)) updateProperty(node, name, undefined)
145
+ for (const name of Object.keys(next)) {
146
+ if (reserved(name)) continue
147
+ const value = next[name]
148
+ if (Object.hasOwn(previous, name)) {
149
+ const old = previous[name]
150
+ if (Object.is(old, value) || (name === 'style' && sameStyle(old, value))) continue
151
+ }
152
+ updateProperty(node, name, value)
153
+ }
154
+ }
155
+
156
+ function createProperties(node: Node, props: Props): void {
157
+ for (const name of Object.keys(props))
158
+ if (!reserved(name)) updateProperty(node, name, props[name])
159
+ }
160
+
161
+ const host: Config = {
162
+ supportsMutation: true,
163
+ supportsPersistence: false,
164
+ supportsHydration: false,
165
+ supportsResources: false,
166
+ supportsSingletons: false,
167
+ supportsTestSelectors: false,
168
+ isPrimaryRenderer: true,
169
+ rendererVersion: '0.0.0',
170
+ rendererPackageName: '@bun-native/react',
171
+ extraDevToolsConfig: null,
172
+ bindToConsole(method, args) {
173
+ const log = method === 'warn' ? console.warn : method === 'error' ? console.error : console.log
174
+ return log.bind(console, ...args)
175
+ },
176
+ createInstance(type, props) {
177
+ // The host config types an element as any string; the renderer rejects unknown tags.
178
+ const node = createElement(type as ElementTag)
179
+ createProperties(node, props)
180
+ return node
181
+ },
182
+ createTextInstance: createTextNode,
183
+ appendInitialChild: insertNode,
184
+ finalizeInitialChildren: returnFalse,
185
+ shouldSetTextContent: returnFalse,
186
+ getRootHostContext: hostContext,
187
+ getChildHostContext: hostContext,
188
+ getPublicInstance: (node) => node,
189
+ prepareForCommit: noInstance,
190
+ resetAfterCommit: noOp,
191
+ preparePortalMount: unsupported.bind(null, 'portals'),
192
+ scheduleTimeout: setTimeout,
193
+ cancelTimeout: clearTimeout,
194
+ noTimeout: null,
195
+ supportsMicrotasks: true,
196
+ scheduleMicrotask: queueMicrotask,
197
+ getInstanceFromNode: noInstance,
198
+ beforeActiveInstanceBlur: noOp,
199
+ afterActiveInstanceBlur: noOp,
200
+ prepareScopeUpdate: unsupported.bind(null, 'scope'),
201
+ getInstanceFromScope: noInstance,
202
+ detachDeletedInstance(node) {
203
+ for (const binding of bindings.get(node)?.values() ?? []) binding.listener = undefined
204
+ bindings.delete(node)
205
+ node.children.length = 0
206
+ node.parent = undefined
207
+ for (const name of Object.keys(node.properties)) {
208
+ if (isEventProperty(name)) delete node.properties[name]
209
+ }
210
+ },
211
+ appendChild: insertNode,
212
+ appendChildToContainer: insertNode,
213
+ insertBefore: insertNode,
214
+ insertInContainerBefore: insertNode,
215
+ removeChild: removeNode,
216
+ removeChildFromContainer: removeNode,
217
+ commitTextUpdate: (node, _previous, next) => replaceText(node, next),
218
+ commitUpdate: (node, _type, previous, next) => updateProperties(node, previous, next),
219
+ hideInstance: unsupported.bind(null, 'hidden-subtrees'),
220
+ hideTextInstance: unsupported.bind(null, 'hidden-subtrees'),
221
+ unhideInstance: unsupported.bind(null, 'hidden-subtrees'),
222
+ unhideTextInstance: unsupported.bind(null, 'hidden-subtrees'),
223
+ clearContainer(container) {
224
+ // Removing the last child pops it; removing the first would leave a hole
225
+ // that the next `children` read compacts, making the loop quadratic.
226
+ const children = container.children
227
+ while (children.length) removeNode(container, children[children.length - 1]!)
228
+ },
229
+ NotPendingTransition: null,
230
+ HostTransitionContext: createContext(null),
231
+ setCurrentUpdatePriority(priority) {
232
+ updatePriority = priority
233
+ },
234
+ getCurrentUpdatePriority: () => updatePriority,
235
+ resolveUpdatePriority: () => updatePriority || DefaultEventPriority,
236
+ resetFormInstance: unsupported.bind(null, 'forms'),
237
+ requestPostPaintCallback: unsupported.bind(null, 'presentation-timing'),
238
+ shouldAttemptEagerTransition: returnFalse,
239
+ trackSchedulerEvent: noOp,
240
+ resolveEventType: () => activeEvent?.type ?? null,
241
+ resolveEventTimeStamp: () => activeEvent?.observedAt ?? -1,
242
+ maySuspendCommit: returnFalse,
243
+ maySuspendCommitOnUpdate: returnFalse,
244
+ maySuspendCommitInSyncRender: returnFalse,
245
+ preloadInstance: () => true,
246
+ startSuspendingCommit: noInstance,
247
+ suspendInstance: unsupported.bind(null, 'suspended-host-commit'),
248
+ // React 19.3 calls this for ordinary transition lanes too. This host has no
249
+ // active view-transition animation whose completion could delay a commit.
250
+ suspendOnActiveViewTransition: noOp,
251
+ createViewTransitionInstance: unsupported.bind(null, 'view-transitions'),
252
+ applyViewTransitionName: unsupported.bind(null, 'view-transitions'),
253
+ startViewTransition: unsupported.bind(null, 'view-transitions'),
254
+ waitForCommitToBeReady: noInstance,
255
+ getSuspendedCommitReason: noInstance,
256
+ }
257
+
258
+ export const reconciler = Reconciler(host)
@@ -0,0 +1,33 @@
1
+ import type { ErrorInfo } from 'react'
2
+
3
+ /** Owned callbacks must be revocable even while React retains an opaque root. */
4
+ export interface RootErrorOwner {
5
+ terminate: ((error: unknown) => void) | undefined
6
+ report: ((error: unknown, info: Readonly<ErrorInfo>) => void) | undefined
7
+ }
8
+
9
+ function ignore(): void {}
10
+
11
+ /** Created outside the Root constructor so callback scopes cannot capture its arguments. */
12
+ export function createRootErrors(owner: RootErrorOwner) {
13
+ return {
14
+ uncaught(error: unknown): void {
15
+ owner.terminate?.(error)
16
+ },
17
+ caught: ignore,
18
+ transition: ignore,
19
+ recoverable(error: unknown, info: ErrorInfo): void {
20
+ const report = owner.report
21
+ if (!report) return
22
+ try {
23
+ report(error, Object.freeze({ ...info }))
24
+ } catch (reporterError) {
25
+ owner.terminate?.(reporterError)
26
+ }
27
+ },
28
+ release(): void {
29
+ owner.terminate = undefined
30
+ owner.report = undefined
31
+ },
32
+ }
33
+ }