onejs-react 0.1.0 → 0.1.2
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/README.md +265 -0
- package/package.json +3 -1
- package/src/__tests__/components.test.tsx +36 -0
- package/src/__tests__/host-config.test.ts +141 -99
- package/src/__tests__/mocks.ts +17 -1
- package/src/__tests__/setup.ts +23 -11
- package/src/components.tsx +77 -48
- package/src/error-boundary.tsx +175 -0
- package/src/host-config.ts +503 -162
- package/src/index.ts +46 -2
- package/src/renderer.ts +50 -23
- package/src/screen.tsx +1 -1
- package/src/style-parser.ts +171 -79
- package/src/types.ts +326 -8
- package/src/vector.ts +312 -0
package/src/__tests__/mocks.ts
CHANGED
|
@@ -148,6 +148,15 @@ export class MockVisualElement {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Mock TextElement - base text element for implicit text content
|
|
153
|
+
*/
|
|
154
|
+
export class MockTextElement extends MockVisualElement {
|
|
155
|
+
constructor() {
|
|
156
|
+
super('UnityEngine.UIElements.TextElement');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
/**
|
|
152
161
|
* Mock Label element
|
|
153
162
|
*/
|
|
@@ -225,6 +234,7 @@ export function createMockCS() {
|
|
|
225
234
|
// UI Elements
|
|
226
235
|
UIElements: {
|
|
227
236
|
VisualElement: MockVisualElement,
|
|
237
|
+
TextElement: MockTextElement,
|
|
228
238
|
Label: MockLabel,
|
|
229
239
|
Button: MockButton,
|
|
230
240
|
TextField: MockTextField,
|
|
@@ -279,7 +289,13 @@ export async function flushMicrotasks(): Promise<void> {
|
|
|
279
289
|
for (let i = 0; i < 50; i++) {
|
|
280
290
|
await Promise.resolve();
|
|
281
291
|
// Also allow any setTimeout callbacks to run
|
|
282
|
-
await new Promise(resolve =>
|
|
292
|
+
await new Promise<void>(resolve => {
|
|
293
|
+
if (typeof setImmediate !== "undefined") {
|
|
294
|
+
setImmediate(resolve);
|
|
295
|
+
} else {
|
|
296
|
+
setTimeout(resolve, 0);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
283
299
|
}
|
|
284
300
|
}
|
|
285
301
|
|
package/src/__tests__/setup.ts
CHANGED
|
@@ -8,23 +8,35 @@
|
|
|
8
8
|
* - console: QuickJS built-in
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { vi } from
|
|
12
|
-
import { createMockCS, resetAllMocks } from
|
|
11
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { createMockCS, resetAllMocks } from "./mocks";
|
|
13
|
+
|
|
14
|
+
// Extend globalThis type for our mocks
|
|
15
|
+
declare global {
|
|
16
|
+
// eslint-disable-next-line no-var
|
|
17
|
+
var CS: ReturnType<typeof createMockCS>;
|
|
18
|
+
// eslint-disable-next-line no-var
|
|
19
|
+
var __eventAPI: {
|
|
20
|
+
addEventListener: ReturnType<typeof vi.fn>;
|
|
21
|
+
removeEventListener: ReturnType<typeof vi.fn>;
|
|
22
|
+
removeAllEventListeners: ReturnType<typeof vi.fn>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
13
25
|
|
|
14
26
|
// Store original globals (Node.js provides these)
|
|
15
|
-
const originalSetTimeout =
|
|
16
|
-
const originalClearTimeout =
|
|
17
|
-
const originalQueueMicrotask =
|
|
27
|
+
const originalSetTimeout = global.setTimeout;
|
|
28
|
+
const originalClearTimeout = global.clearTimeout;
|
|
29
|
+
const originalQueueMicrotask = global.queueMicrotask;
|
|
18
30
|
|
|
19
31
|
// Set up globals before each test
|
|
20
32
|
beforeEach(() => {
|
|
21
33
|
resetAllMocks();
|
|
22
34
|
|
|
23
35
|
// Create fresh mock CS global
|
|
24
|
-
|
|
36
|
+
global.CS = createMockCS();
|
|
25
37
|
|
|
26
38
|
// Mock event API with spies
|
|
27
|
-
|
|
39
|
+
global.__eventAPI = {
|
|
28
40
|
addEventListener: vi.fn(),
|
|
29
41
|
removeEventListener: vi.fn(),
|
|
30
42
|
removeAllEventListeners: vi.fn(),
|
|
@@ -32,18 +44,18 @@ beforeEach(() => {
|
|
|
32
44
|
|
|
33
45
|
// Use real console but spy on it for test assertions
|
|
34
46
|
// (React reconciler logs things we want to see during debugging)
|
|
35
|
-
(
|
|
47
|
+
(global as any).console = {
|
|
36
48
|
log: vi.fn(),
|
|
37
49
|
error: vi.fn(),
|
|
38
50
|
warn: vi.fn(),
|
|
39
51
|
};
|
|
40
52
|
|
|
41
53
|
// Use real queueMicrotask - React scheduler depends on it
|
|
42
|
-
(
|
|
54
|
+
(global as any).queueMicrotask = originalQueueMicrotask || ((cb: () => void) => Promise.resolve().then(cb));
|
|
43
55
|
|
|
44
56
|
// Use real setTimeout/clearTimeout - React scheduler depends on them
|
|
45
|
-
(
|
|
46
|
-
(
|
|
57
|
+
(global as any).setTimeout = originalSetTimeout;
|
|
58
|
+
(global as any).clearTimeout = originalClearTimeout;
|
|
47
59
|
});
|
|
48
60
|
|
|
49
61
|
// Clean up after each test
|
package/src/components.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { forwardRef, type ReactElement, type Ref } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
ViewProps,
|
|
4
|
+
TextProps,
|
|
4
5
|
LabelProps,
|
|
5
6
|
ButtonProps,
|
|
6
7
|
TextFieldProps,
|
|
@@ -8,24 +9,37 @@ import type {
|
|
|
8
9
|
SliderProps,
|
|
9
10
|
ScrollViewProps,
|
|
10
11
|
ImageProps,
|
|
11
|
-
ListViewProps
|
|
12
|
+
ListViewProps,
|
|
13
|
+
VisualElement,
|
|
14
|
+
TextElement,
|
|
15
|
+
LabelElement,
|
|
16
|
+
ButtonElement,
|
|
17
|
+
TextFieldElement,
|
|
18
|
+
ToggleElement,
|
|
19
|
+
SliderElement,
|
|
20
|
+
ScrollViewElement,
|
|
21
|
+
ImageElement,
|
|
12
22
|
} from './types';
|
|
13
23
|
|
|
24
|
+
// Props with ref support for intrinsic elements
|
|
25
|
+
type WithRef<Props, Element> = Props & { ref?: Ref<Element> };
|
|
26
|
+
|
|
14
27
|
// Declare the intrinsic element types for JSX
|
|
15
28
|
// Using 'ojs-' prefix to avoid conflicts with HTML/SVG element names in @types/react
|
|
16
29
|
// For React 19 with jsx: "react-jsx", we need to augment 'react/jsx-runtime'
|
|
17
30
|
declare module 'react/jsx-runtime' {
|
|
18
31
|
namespace JSX {
|
|
19
32
|
interface IntrinsicElements {
|
|
20
|
-
'ojs-view': ViewProps
|
|
21
|
-
'ojs-
|
|
22
|
-
'ojs-
|
|
23
|
-
'ojs-
|
|
24
|
-
'ojs-
|
|
25
|
-
'ojs-
|
|
26
|
-
'ojs-
|
|
27
|
-
'ojs-
|
|
28
|
-
'ojs-
|
|
33
|
+
'ojs-view': WithRef<ViewProps, VisualElement>;
|
|
34
|
+
'ojs-text': WithRef<TextProps, TextElement>;
|
|
35
|
+
'ojs-label': WithRef<LabelProps, LabelElement>;
|
|
36
|
+
'ojs-button': WithRef<ButtonProps, ButtonElement>;
|
|
37
|
+
'ojs-textfield': WithRef<TextFieldProps, TextFieldElement>;
|
|
38
|
+
'ojs-toggle': WithRef<ToggleProps, ToggleElement>;
|
|
39
|
+
'ojs-slider': WithRef<SliderProps, SliderElement>;
|
|
40
|
+
'ojs-scrollview': WithRef<ScrollViewProps, ScrollViewElement>;
|
|
41
|
+
'ojs-image': WithRef<ImageProps, ImageElement>;
|
|
42
|
+
'ojs-listview': WithRef<ListViewProps, VisualElement>;
|
|
29
43
|
}
|
|
30
44
|
}
|
|
31
45
|
}
|
|
@@ -34,54 +48,69 @@ declare module 'react/jsx-runtime' {
|
|
|
34
48
|
declare module 'react' {
|
|
35
49
|
namespace JSX {
|
|
36
50
|
interface IntrinsicElements {
|
|
37
|
-
'ojs-view': ViewProps
|
|
38
|
-
'ojs-
|
|
39
|
-
'ojs-
|
|
40
|
-
'ojs-
|
|
41
|
-
'ojs-
|
|
42
|
-
'ojs-
|
|
43
|
-
'ojs-
|
|
44
|
-
'ojs-
|
|
45
|
-
'ojs-
|
|
51
|
+
'ojs-view': WithRef<ViewProps, VisualElement>;
|
|
52
|
+
'ojs-text': WithRef<TextProps, TextElement>;
|
|
53
|
+
'ojs-label': WithRef<LabelProps, LabelElement>;
|
|
54
|
+
'ojs-button': WithRef<ButtonProps, ButtonElement>;
|
|
55
|
+
'ojs-textfield': WithRef<TextFieldProps, TextFieldElement>;
|
|
56
|
+
'ojs-toggle': WithRef<ToggleProps, ToggleElement>;
|
|
57
|
+
'ojs-slider': WithRef<SliderProps, SliderElement>;
|
|
58
|
+
'ojs-scrollview': WithRef<ScrollViewProps, ScrollViewElement>;
|
|
59
|
+
'ojs-image': WithRef<ImageProps, ImageElement>;
|
|
60
|
+
'ojs-listview': WithRef<ListViewProps, VisualElement>;
|
|
46
61
|
}
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
// Component wrappers that provide nice capitalized names
|
|
51
|
-
// These
|
|
66
|
+
// These use forwardRef to pass refs through to the intrinsic elements
|
|
52
67
|
|
|
53
|
-
export
|
|
54
|
-
return <ojs-view {...props} />;
|
|
55
|
-
}
|
|
68
|
+
export const View = forwardRef<VisualElement, ViewProps>((props, ref) => {
|
|
69
|
+
return <ojs-view ref={ref} {...props} />;
|
|
70
|
+
});
|
|
71
|
+
View.displayName = 'View';
|
|
56
72
|
|
|
57
|
-
export
|
|
58
|
-
return <ojs-
|
|
59
|
-
}
|
|
73
|
+
export const Text = forwardRef<TextElement, TextProps>((props, ref) => {
|
|
74
|
+
return <ojs-text ref={ref} {...props} />;
|
|
75
|
+
});
|
|
76
|
+
Text.displayName = 'Text';
|
|
60
77
|
|
|
61
|
-
export
|
|
62
|
-
return <ojs-
|
|
63
|
-
}
|
|
78
|
+
export const Label = forwardRef<LabelElement, LabelProps>((props, ref) => {
|
|
79
|
+
return <ojs-label ref={ref} {...props} />;
|
|
80
|
+
});
|
|
81
|
+
Label.displayName = 'Label';
|
|
64
82
|
|
|
65
|
-
export
|
|
66
|
-
return <ojs-
|
|
67
|
-
}
|
|
83
|
+
export const Button = forwardRef<ButtonElement, ButtonProps>((props, ref) => {
|
|
84
|
+
return <ojs-button ref={ref} {...props} />;
|
|
85
|
+
});
|
|
86
|
+
Button.displayName = 'Button';
|
|
68
87
|
|
|
69
|
-
export
|
|
70
|
-
return <ojs-
|
|
71
|
-
}
|
|
88
|
+
export const TextField = forwardRef<TextFieldElement, TextFieldProps>((props, ref) => {
|
|
89
|
+
return <ojs-textfield ref={ref} {...props} />;
|
|
90
|
+
});
|
|
91
|
+
TextField.displayName = 'TextField';
|
|
72
92
|
|
|
73
|
-
export
|
|
74
|
-
return <ojs-
|
|
75
|
-
}
|
|
93
|
+
export const Toggle = forwardRef<ToggleElement, ToggleProps>((props, ref) => {
|
|
94
|
+
return <ojs-toggle ref={ref} {...props} />;
|
|
95
|
+
});
|
|
96
|
+
Toggle.displayName = 'Toggle';
|
|
76
97
|
|
|
77
|
-
export
|
|
78
|
-
return <ojs-
|
|
79
|
-
}
|
|
98
|
+
export const Slider = forwardRef<SliderElement, SliderProps>((props, ref) => {
|
|
99
|
+
return <ojs-slider ref={ref} {...props} />;
|
|
100
|
+
});
|
|
101
|
+
Slider.displayName = 'Slider';
|
|
80
102
|
|
|
81
|
-
export
|
|
82
|
-
return <ojs-
|
|
83
|
-
}
|
|
103
|
+
export const ScrollView = forwardRef<ScrollViewElement, ScrollViewProps>((props, ref) => {
|
|
104
|
+
return <ojs-scrollview ref={ref} {...props} />;
|
|
105
|
+
});
|
|
106
|
+
ScrollView.displayName = 'ScrollView';
|
|
84
107
|
|
|
85
|
-
export
|
|
86
|
-
return <ojs-
|
|
87
|
-
}
|
|
108
|
+
export const Image = forwardRef<ImageElement, ImageProps>((props, ref) => {
|
|
109
|
+
return <ojs-image ref={ref} {...props} />;
|
|
110
|
+
});
|
|
111
|
+
Image.displayName = 'Image';
|
|
112
|
+
|
|
113
|
+
export const ListView = forwardRef<VisualElement, ListViewProps>((props, ref) => {
|
|
114
|
+
return <ojs-listview ref={ref} {...props} />;
|
|
115
|
+
});
|
|
116
|
+
ListView.displayName = 'ListView';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Component, type ReactNode, type ErrorInfo } from "react"
|
|
2
|
+
import { View, Label } from "./components"
|
|
3
|
+
|
|
4
|
+
declare const console: { log: (...args: unknown[]) => void; error: (...args: unknown[]) => void }
|
|
5
|
+
|
|
6
|
+
export interface ErrorBoundaryProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
/**
|
|
9
|
+
* Optional fallback to render when an error occurs.
|
|
10
|
+
* Can be a ReactNode or a function that receives error details.
|
|
11
|
+
*/
|
|
12
|
+
fallback?: ReactNode | ((error: Error, errorInfo: ErrorInfo) => ReactNode)
|
|
13
|
+
/**
|
|
14
|
+
* Optional callback when an error is caught.
|
|
15
|
+
* Use this for logging or error reporting.
|
|
16
|
+
*/
|
|
17
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
|
18
|
+
/**
|
|
19
|
+
* Optional callback when the boundary resets.
|
|
20
|
+
*/
|
|
21
|
+
onReset?: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ErrorBoundaryState {
|
|
25
|
+
hasError: boolean
|
|
26
|
+
error: Error | null
|
|
27
|
+
errorInfo: ErrorInfo | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Error boundary component for catching and displaying React errors.
|
|
32
|
+
*
|
|
33
|
+
* @example Basic usage with default fallback
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <ErrorBoundary>
|
|
36
|
+
* <MyComponent />
|
|
37
|
+
* </ErrorBoundary>
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example Custom fallback UI
|
|
41
|
+
* ```tsx
|
|
42
|
+
* <ErrorBoundary fallback={<Label>Something went wrong</Label>}>
|
|
43
|
+
* <MyComponent />
|
|
44
|
+
* </ErrorBoundary>
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example Fallback function with error details
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <ErrorBoundary
|
|
50
|
+
* fallback={(error, info) => (
|
|
51
|
+
* <View>
|
|
52
|
+
* <Label>Error: {error.message}</Label>
|
|
53
|
+
* <Label>Stack: {info.componentStack}</Label>
|
|
54
|
+
* </View>
|
|
55
|
+
* )}
|
|
56
|
+
* >
|
|
57
|
+
* <MyComponent />
|
|
58
|
+
* </ErrorBoundary>
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example Error logging
|
|
62
|
+
* ```tsx
|
|
63
|
+
* <ErrorBoundary onError={(error, info) => logError(error, info)}>
|
|
64
|
+
* <MyComponent />
|
|
65
|
+
* </ErrorBoundary>
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
69
|
+
constructor(props: ErrorBoundaryProps) {
|
|
70
|
+
super(props)
|
|
71
|
+
this.state = {
|
|
72
|
+
hasError: false,
|
|
73
|
+
error: null,
|
|
74
|
+
errorInfo: null,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
79
|
+
return { hasError: true, error }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
83
|
+
this.setState({ errorInfo })
|
|
84
|
+
|
|
85
|
+
// Log to console with helpful formatting
|
|
86
|
+
console.error("[OneJS React] Error caught by ErrorBoundary:")
|
|
87
|
+
console.error(" Error:", error.message)
|
|
88
|
+
if (error.stack) {
|
|
89
|
+
console.error(" Stack:", error.stack)
|
|
90
|
+
}
|
|
91
|
+
if (errorInfo.componentStack) {
|
|
92
|
+
console.error(" Component Stack:", errorInfo.componentStack)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Call user-provided error handler
|
|
96
|
+
this.props.onError?.(error, errorInfo)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reset the error boundary to try rendering children again.
|
|
101
|
+
* Useful after the underlying issue has been fixed.
|
|
102
|
+
*/
|
|
103
|
+
reset = (): void => {
|
|
104
|
+
this.setState({
|
|
105
|
+
hasError: false,
|
|
106
|
+
error: null,
|
|
107
|
+
errorInfo: null,
|
|
108
|
+
})
|
|
109
|
+
this.props.onReset?.()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render(): ReactNode {
|
|
113
|
+
if (this.state.hasError) {
|
|
114
|
+
const { fallback } = this.props
|
|
115
|
+
const { error, errorInfo } = this.state
|
|
116
|
+
|
|
117
|
+
// Render custom fallback if provided
|
|
118
|
+
if (fallback !== undefined) {
|
|
119
|
+
if (typeof fallback === "function") {
|
|
120
|
+
return fallback(error!, errorInfo!)
|
|
121
|
+
}
|
|
122
|
+
return fallback
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default fallback UI
|
|
126
|
+
return (
|
|
127
|
+
<View style={{
|
|
128
|
+
padding: 16,
|
|
129
|
+
backgroundColor: "#2d1b1b",
|
|
130
|
+
borderColor: "#ff4444",
|
|
131
|
+
borderWidth: 2,
|
|
132
|
+
}}>
|
|
133
|
+
<Label style={{
|
|
134
|
+
color: "#ff6666",
|
|
135
|
+
fontSize: 16,
|
|
136
|
+
marginBottom: 8,
|
|
137
|
+
}}>
|
|
138
|
+
Something went wrong
|
|
139
|
+
</Label>
|
|
140
|
+
<Label style={{
|
|
141
|
+
color: "#ffaaaa",
|
|
142
|
+
fontSize: 12,
|
|
143
|
+
}}>
|
|
144
|
+
{error?.message || "Unknown error"}
|
|
145
|
+
</Label>
|
|
146
|
+
</View>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.props.children
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Helper hook-style error info for functional components.
|
|
156
|
+
* Note: This is a simple utility. For actual error catching,
|
|
157
|
+
* you must use the ErrorBoundary class component.
|
|
158
|
+
*/
|
|
159
|
+
export function formatError(error: Error, componentStack?: string): string {
|
|
160
|
+
const parts = [`Error: ${error.message}`]
|
|
161
|
+
|
|
162
|
+
if (error.stack) {
|
|
163
|
+
// Get first few lines of stack
|
|
164
|
+
const stackLines = error.stack.split("\n").slice(0, 5)
|
|
165
|
+
parts.push(`Stack:\n${stackLines.join("\n")}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (componentStack) {
|
|
169
|
+
// Get first few lines of component stack
|
|
170
|
+
const compLines = componentStack.split("\n").slice(0, 5)
|
|
171
|
+
parts.push(`Component:\n${compLines.join("\n")}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return parts.join("\n\n")
|
|
175
|
+
}
|