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", () => {...@@ -58,10 +58,23 @@ describe("resolveNativeReviewDiffView", () => {
5858
59 it("returns null when the view manager cannot be required", async () => {59 it("returns null when the view manager cannot be required", async () => {
60 setExpoViewConfigAvailable();60 setExpoViewConfigAvailable();
61 const cause = new Error("boom");
61 expoMocks.requireNativeView.mockImplementation(() => {62 expoMocks.requireNativeView.mockImplementation(() => {
62 throw new Error("boom");63 throw cause;
63 });64 });
65 const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
64 const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface");66 const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface");
67
68 expect(resolveNativeReviewDiffView()).toBeNull();
65 expect(resolveNativeReviewDiffView()).toBeNull();69 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);
66 });79 });
67});80});
apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts+15-1
...@@ -2,6 +2,8 @@ import type { ComponentType } from "react";...@@ -2,6 +2,8 @@ import type { ComponentType } from "react";
2import type { NativeSyntheticEvent, ViewProps } from "react-native";2import type { NativeSyntheticEvent, ViewProps } from "react-native";
3import { requireNativeView } from "expo";3import { requireNativeView } from "expo";
44
5import { NativeViewResolutionError } from "../../native/nativeViewResolutionError";
6
5const NATIVE_REVIEW_DIFF_MODULE_NAME = "T3ReviewDiffSurface";7const NATIVE_REVIEW_DIFF_MODULE_NAME = "T3ReviewDiffSurface";
68
7interface ExpoGlobalWithViewConfig {9interface ExpoGlobalWithViewConfig {
...@@ -128,6 +130,7 @@ export interface NativeReviewDiffViewProps extends ViewProps {...@@ -128,6 +130,7 @@ export interface NativeReviewDiffViewProps extends ViewProps {
128}130}
129131
130let cachedNativeReviewDiffView: ComponentType<NativeReviewDiffViewProps> | undefined;132let cachedNativeReviewDiffView: ComponentType<NativeReviewDiffViewProps> | undefined;
133let nativeReviewDiffViewResolutionFailed = false;
131134
132function getExpoViewConfig(moduleName: string) {135function getExpoViewConfig(moduleName: string) {
133 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(136 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(
...@@ -140,6 +143,10 @@ export function resolveNativeReviewDiffView(): ComponentType<NativeReviewDiffVie...@@ -140,6 +143,10 @@ export function resolveNativeReviewDiffView(): ComponentType<NativeReviewDiffVie
140 return cachedNativeReviewDiffView;143 return cachedNativeReviewDiffView;
141 }144 }
142145
146 if (nativeReviewDiffViewResolutionFailed) {
147 return null;
148 }
149
143 if (getExpoViewConfig(NATIVE_REVIEW_DIFF_MODULE_NAME) == null) {150 if (getExpoViewConfig(NATIVE_REVIEW_DIFF_MODULE_NAME) == null) {
144 return null;151 return null;
145 }152 }
...@@ -148,7 +155,14 @@ export function resolveNativeReviewDiffView(): ComponentType<NativeReviewDiffVie...@@ -148,7 +155,14 @@ export function resolveNativeReviewDiffView(): ComponentType<NativeReviewDiffVie
148 cachedNativeReviewDiffView = requireNativeView<NativeReviewDiffViewProps>(155 cachedNativeReviewDiffView = requireNativeView<NativeReviewDiffViewProps>(
149 NATIVE_REVIEW_DIFF_MODULE_NAME,156 NATIVE_REVIEW_DIFF_MODULE_NAME,
150 );157 );
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 );
152 return null;166 return null;
153 }167 }
154168
apps/mobile/src/features/terminal/nativeTerminalModule.test.ts+14-1
...@@ -43,10 +43,23 @@ describe("resolveNativeTerminalSurfaceView", () => {...@@ -43,10 +43,23 @@ describe("resolveNativeTerminalSurfaceView", () => {
4343
44 it("returns null when the view manager cannot be required", async () => {44 it("returns null when the view manager cannot be required", async () => {
45 setExpoViewConfigAvailable();45 setExpoViewConfigAvailable();
46 const cause = new Error("boom");
46 expoMocks.requireNativeView.mockImplementation(() => {47 expoMocks.requireNativeView.mockImplementation(() => {
47 throw new Error("boom");48 throw cause;
48 });49 });
50 const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
49 const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule");51 const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule");
52
53 expect(resolveNativeTerminalSurfaceView()).toBeNull();
50 expect(resolveNativeTerminalSurfaceView()).toBeNull();54 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);
51 });64 });
52});65});
apps/mobile/src/features/terminal/nativeTerminalModule.ts+15-1
...@@ -2,6 +2,8 @@ import type { ComponentType } from "react";...@@ -2,6 +2,8 @@ import type { ComponentType } from "react";
2import type { NativeSyntheticEvent, ViewProps } from "react-native";2import type { NativeSyntheticEvent, ViewProps } from "react-native";
3import { requireNativeView } from "expo";3import { requireNativeView } from "expo";
44
5import { NativeViewResolutionError } from "../../native/nativeViewResolutionError";
6
5const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface";7const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface";
68
7interface ExpoGlobalWithViewConfig {9interface ExpoGlobalWithViewConfig {
...@@ -33,6 +35,7 @@ export interface NativeTerminalSurfaceProps extends ViewProps {...@@ -33,6 +35,7 @@ export interface NativeTerminalSurfaceProps extends ViewProps {
33}35}
3436
35let cachedNativeTerminalSurfaceView: ComponentType<NativeTerminalSurfaceProps> | undefined;37let cachedNativeTerminalSurfaceView: ComponentType<NativeTerminalSurfaceProps> | undefined;
38let nativeTerminalSurfaceViewResolutionFailed = false;
3639
37function getExpoViewConfig(moduleName: string) {40function getExpoViewConfig(moduleName: string) {
38 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(41 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(
...@@ -45,6 +48,10 @@ export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminal...@@ -45,6 +48,10 @@ export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminal
45 return cachedNativeTerminalSurfaceView;48 return cachedNativeTerminalSurfaceView;
46 }49 }
4750
51 if (nativeTerminalSurfaceViewResolutionFailed) {
52 return null;
53 }
54
48 if (getExpoViewConfig(NATIVE_TERMINAL_MODULE_NAME) == null) {55 if (getExpoViewConfig(NATIVE_TERMINAL_MODULE_NAME) == null) {
49 return null;56 return null;
50 }57 }
...@@ -53,7 +60,14 @@ export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminal...@@ -53,7 +60,14 @@ export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminal
53 cachedNativeTerminalSurfaceView = requireNativeView<NativeTerminalSurfaceProps>(60 cachedNativeTerminalSurfaceView = requireNativeView<NativeTerminalSurfaceProps>(
54 NATIVE_TERMINAL_MODULE_NAME,61 NATIVE_TERMINAL_MODULE_NAME,
55 );62 );
56 } catch {63 } catch (cause) {
64 nativeTerminalSurfaceViewResolutionFailed = true;
65 console.error(
66 new NativeViewResolutionError({
67 nativeModuleName: NATIVE_TERMINAL_MODULE_NAME,
68 cause,
69 }),
70 );
57 return null;71 return null;
58 }72 }
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}