authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:06:08-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:06:08-07:00
log779c2374876884992c7cdf059556acd255db824e
tree49f9f309df70cfb998afac8f800e1e3668dbdacb
parent350e229f7a5d5c4cdc9a29cf2d1f911ef27a9a48
signature Signed by PGP key B5690EEEBB952194

[codex] Structure native view resolution failures (#3353)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 71 insertions(+), 4 deletions(-)

apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts+14-1
......@@ -58,10 +58,23 @@ describe("resolveNativeReviewDiffView", () => {
5858
5959 it("returns null when the view manager cannot be required", async () => {
6060 setExpoViewConfigAvailable();
61 const cause = new Error("boom");
6162 expoMocks.requireNativeView.mockImplementation(() => {
62 throw new Error("boom");
63 throw cause;
6364 });
65 const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
6466 const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface");
67
68 expect(resolveNativeReviewDiffView()).toBeNull();
6569 expect(resolveNativeReviewDiffView()).toBeNull();
70 expect(expoMocks.requireNativeView).toHaveBeenCalledTimes(1);
71 expect(consoleError).toHaveBeenCalledWith(
72 expect.objectContaining({
73 _tag: "NativeViewResolutionError",
74 nativeModuleName: "T3ReviewDiffSurface",
75 cause,
76 }),
77 );
78 expect(consoleError).toHaveBeenCalledTimes(1);
6679 });
6780});
apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts+15-1
......@@ -2,6 +2,8 @@ import type { ComponentType } from "react";
22import type { NativeSyntheticEvent, ViewProps } from "react-native";
33import { requireNativeView } from "expo";
44
5import { NativeViewResolutionError } from "../../native/nativeViewResolutionError";
6
57const NATIVE_REVIEW_DIFF_MODULE_NAME = "T3ReviewDiffSurface";
68
79interface ExpoGlobalWithViewConfig {
......@@ -128,6 +130,7 @@ export interface NativeReviewDiffViewProps extends ViewProps {
128130}
129131
130132let cachedNativeReviewDiffView: ComponentType<NativeReviewDiffViewProps> | undefined;
133let nativeReviewDiffViewResolutionFailed = false;
131134
132135function getExpoViewConfig(moduleName: string) {
133136 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(
......@@ -140,6 +143,10 @@ export function resolveNativeReviewDiffView(): ComponentType<NativeReviewDiffVie
140143 return cachedNativeReviewDiffView;
141144 }
142145
146 if (nativeReviewDiffViewResolutionFailed) {
147 return null;
148 }
149
143150 if (getExpoViewConfig(NATIVE_REVIEW_DIFF_MODULE_NAME) == null) {
144151 return null;
145152 }
......@@ -148,7 +155,14 @@ export function resolveNativeReviewDiffView(): ComponentType<NativeReviewDiffVie
148155 cachedNativeReviewDiffView = requireNativeView<NativeReviewDiffViewProps>(
149156 NATIVE_REVIEW_DIFF_MODULE_NAME,
150157 );
151 } catch {
158 } catch (cause) {
159 nativeReviewDiffViewResolutionFailed = true;
160 console.error(
161 new NativeViewResolutionError({
162 nativeModuleName: NATIVE_REVIEW_DIFF_MODULE_NAME,
163 cause,
164 }),
165 );
152166 return null;
153167 }
154168
apps/mobile/src/features/terminal/nativeTerminalModule.test.ts+14-1
......@@ -43,10 +43,23 @@ describe("resolveNativeTerminalSurfaceView", () => {
4343
4444 it("returns null when the view manager cannot be required", async () => {
4545 setExpoViewConfigAvailable();
46 const cause = new Error("boom");
4647 expoMocks.requireNativeView.mockImplementation(() => {
47 throw new Error("boom");
48 throw cause;
4849 });
50 const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
4951 const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule");
52
53 expect(resolveNativeTerminalSurfaceView()).toBeNull();
5054 expect(resolveNativeTerminalSurfaceView()).toBeNull();
55 expect(expoMocks.requireNativeView).toHaveBeenCalledTimes(1);
56 expect(consoleError).toHaveBeenCalledWith(
57 expect.objectContaining({
58 _tag: "NativeViewResolutionError",
59 nativeModuleName: "T3TerminalSurface",
60 cause,
61 }),
62 );
63 expect(consoleError).toHaveBeenCalledTimes(1);
5164 });
5265});
apps/mobile/src/features/terminal/nativeTerminalModule.ts+15-1
......@@ -2,6 +2,8 @@ import type { ComponentType } from "react";
22import type { NativeSyntheticEvent, ViewProps } from "react-native";
33import { requireNativeView } from "expo";
44
5import { NativeViewResolutionError } from "../../native/nativeViewResolutionError";
6
57const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface";
68
79interface ExpoGlobalWithViewConfig {
......@@ -33,6 +35,7 @@ export interface NativeTerminalSurfaceProps extends ViewProps {
3335}
3436
3537let cachedNativeTerminalSurfaceView: ComponentType<NativeTerminalSurfaceProps> | undefined;
38let nativeTerminalSurfaceViewResolutionFailed = false;
3639
3740function getExpoViewConfig(moduleName: string) {
3841 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(
......@@ -45,6 +48,10 @@ export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminal
4548 return cachedNativeTerminalSurfaceView;
4649 }
4750
51 if (nativeTerminalSurfaceViewResolutionFailed) {
52 return null;
53 }
54
4855 if (getExpoViewConfig(NATIVE_TERMINAL_MODULE_NAME) == null) {
4956 return null;
5057 }
......@@ -53,7 +60,14 @@ export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminal
5360 cachedNativeTerminalSurfaceView = requireNativeView<NativeTerminalSurfaceProps>(
5461 NATIVE_TERMINAL_MODULE_NAME,
5562 );
56 } catch {
63 } catch (cause) {
64 nativeTerminalSurfaceViewResolutionFailed = true;
65 console.error(
66 new NativeViewResolutionError({
67 nativeModuleName: NATIVE_TERMINAL_MODULE_NAME,
68 cause,
69 }),
70 );
5771 return null;
5872 }
5973
apps/mobile/src/native/nativeViewResolutionError.ts created+13
......@@ -0,0 +1,13 @@
1import * as Schema from "effect/Schema";
2
3export class NativeViewResolutionError extends Schema.TaggedErrorClass<NativeViewResolutionError>()(
4 "NativeViewResolutionError",
5 {
6 nativeModuleName: Schema.String,
7 cause: Schema.Defect(),
8 },
9) {
10 override get message(): string {
11 return `Failed to resolve native view ${this.nativeModuleName}.`;
12 }
13}