From e867773932c7b8522c2f09a4d499a88bbf18ebab Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:59:25 +0200 Subject: [PATCH] Restore native app startup (#6734) * fix(app): install navigator polyfill before any app module loads @xterm/headless reads navigator.userAgent at module evaluation. Since #6455 the root layout transitively imports it, so the layout's module-level polyfillNavigator() ran too late and native builds crashed at launch with "Cannot read property 'includes' of undefined". Install the crypto, navigator and screen.orientation polyfills from a side-effect module imported first by index.ts. Calls written in index.ts itself ran after all hoisted imports had evaluated. Fixes #6504 Co-Authored-By: Claude Opus 5.5 * test(app): isolate native startup regression from shared globals Exercise the real navigator polyfill and terminal dependency in fresh VM contexts, including the pre-initialization failure. Preserve a check that entry imports install navigator before the app loads. Co-authored-by: panrafal * test(app): follow startup import lint rule --------- Co-authored-by: panrafal Co-authored-by: Claude Opus 5.5 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/app/index.ts | 10 +--- packages/app/src/app/_layout.tsx | 2 - packages/app/src/polyfills/install-early.ts | 12 ++++ .../src/polyfills/navigator-startup.test.ts | 56 +++++++++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 packages/app/src/polyfills/install-early.ts create mode 100644 packages/app/src/polyfills/navigator-startup.test.ts diff --git a/packages/app/index.ts b/packages/app/index.ts index 03b518fd2c..82d47bfe8c 100644 --- a/packages/app/index.ts +++ b/packages/app/index.ts @@ -1,10 +1,6 @@ -// Polyfill crypto.randomUUID for React Native before any other imports -import { polyfillCrypto } from "./src/polyfills/crypto"; -polyfillCrypto(); - -// Polyfill screen.orientation for WebKitGTK desktop runtimes that lack the API. -import { polyfillScreenOrientation } from "./src/polyfills/screen-orientation"; -polyfillScreenOrientation(); +// Install runtime polyfills before any other module evaluates. Keep this the first import. +// oxlint-disable-next-line import/no-unassigned-import -- Polyfills install on import. +import "./src/polyfills/install-early"; // Configure Unistyles before Expo Router pulls in any components using StyleSheet. import "./src/styles/unistyles"; diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 9c028aa2ce..869c93a0b2 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -103,7 +103,6 @@ import { useKeyboardActionDispatcher, } from "@/keyboard/keyboard-action-dispatcher-context"; import { polyfillCrypto } from "@/polyfills/crypto"; -import { polyfillNavigator } from "@/polyfills/navigator"; import { getHostRuntimeStore, hasConfiguredLocalDaemonOverride, @@ -139,7 +138,6 @@ import { type WebNotificationClickDetail, } from "@/utils/os-notifications"; -polyfillNavigator(); polyfillCrypto(); export interface HostRuntimeBootstrapState { diff --git a/packages/app/src/polyfills/install-early.ts b/packages/app/src/polyfills/install-early.ts new file mode 100644 index 0000000000..0ee6ca02fa --- /dev/null +++ b/packages/app/src/polyfills/install-early.ts @@ -0,0 +1,12 @@ +// Side-effect module imported first by index.ts. Babel hoists imports above +// statements, so polyfill calls written in index.ts would run only after every +// imported module had evaluated. Modules that read these globals at load time, +// such as @xterm/headless reading navigator.userAgent, need them installed here. +import { polyfillCrypto } from "./crypto"; +import { polyfillNavigator } from "./navigator"; +import { polyfillScreenOrientation } from "./screen-orientation"; + +polyfillCrypto(); +polyfillNavigator(); +// Polyfill screen.orientation for WebKitGTK desktop runtimes that lack the API. +polyfillScreenOrientation(); diff --git a/packages/app/src/polyfills/navigator-startup.test.ts b/packages/app/src/polyfills/navigator-startup.test.ts new file mode 100644 index 0000000000..49a0fc69a2 --- /dev/null +++ b/packages/app/src/polyfills/navigator-startup.test.ts @@ -0,0 +1,56 @@ +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { createContext, runInContext } from "node:vm"; +import ts from "typescript"; +import { expect, it } from "vitest"; + +it("installs navigator before the app entry loads its other dependencies", () => { + const entry = ts.createSourceFile( + "index.ts", + readFileSync(new URL("../../index.ts", import.meta.url), "utf8"), + ts.ScriptTarget.Latest, + ); + const firstImport = entry.statements.find(ts.isImportDeclaration); + expect(firstImport?.moduleSpecifier.getText(entry)).toBe('"./src/polyfills/install-early"'); + const early = ts.createSourceFile( + "install-early.ts", + readFileSync(new URL("./install-early.ts", import.meta.url), "utf8"), + ts.ScriptTarget.Latest, + ); + expect( + early.statements.some( + (statement) => + ts.isExpressionStatement(statement) && + ts.isCallExpression(statement.expression) && + statement.expression.expression.getText(early) === "polyfillNavigator", + ), + ).toBe(true); +}); + +it("initializes the native navigator before evaluating the real terminal dependency", () => { + const require = createRequire(import.meta.url); + const terminalSource = readFileSync(require.resolve("@xterm/headless"), "utf8"); + const navigatorSource = ts.transpileModule( + readFileSync(new URL("./navigator.ts", import.meta.url), "utf8"), + { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }, + ).outputText; + // Each VM owns its globals, so the native navigator shape never replaces + // globals in the test runner or needs a native-module mock. + const uninitialized = createContext({ + navigator: {}, + window: {}, + module: { exports: {} }, + exports: {}, + }); + expect(() => runInContext(terminalSource, uninitialized)).toThrow(/includes/); + + const initialized = createContext({ + navigator: {}, + window: {}, + module: { exports: {} }, + exports: {}, + }); + runInContext(navigatorSource, initialized); + runInContext("exports.polyfillNavigator()", initialized); + expect(() => runInContext(terminalSource, initialized)).not.toThrow(); +});