From 53b2272a19b1c5a7bcd85358f54435305c7bddb7 Mon Sep 17 00:00:00 2001 From: Yiming Li Date: Wed, 16 Sep 2026 11:44:03 +0800 Subject: [PATCH] fix: polyfill the builtins missing from Lynx's JS engine --- .changeset/lynx-builtin-polyfills.md | 5 ++ packages/lynx/README.md | 5 ++ packages/lynx/src/index.tsx | 2 + packages/lynx/src/polyfills.ts | 91 ++++++++++++++++++++++++++++ packages/lynx/src/stack/index.tsx | 2 + 5 files changed, 105 insertions(+) create mode 100644 .changeset/lynx-builtin-polyfills.md create mode 100644 packages/lynx/src/polyfills.ts diff --git a/.changeset/lynx-builtin-polyfills.md b/.changeset/lynx-builtin-polyfills.md new file mode 100644 index 0000000..cc34a16 --- /dev/null +++ b/.changeset/lynx-builtin-polyfills.md @@ -0,0 +1,5 @@ +--- +'@react-navigation/lynx': patch +--- + +Polyfill the builtins Lynx's JS engine doesn't implement. `@react-navigation/core`, `@react-navigation/routers` and `query-string` call `Array.prototype.findLast`, `findLastIndex` and `at`, `Object.hasOwn`, `String.prototype.replaceAll` and `queueMicrotask`, so navigating a stack crashed with `findLast is not a function`. diff --git a/packages/lynx/README.md b/packages/lynx/README.md index e5cd5d8..4b170a0 100644 --- a/packages/lynx/README.md +++ b/packages/lynx/README.md @@ -23,6 +23,11 @@ host app as well. `@lynx-js/react` has to be 0.126 or later, where `ref` reaches components as a prop the way it does in React 19; `NavigationContainer` depends on that. +Lynx's JS engine is missing builtins that `@react-navigation/core`, +`@react-navigation/routers` and `query-string` call, so importing this package +installs conditional polyfills for `Array.prototype.findLast`, `findLastIndex` +and `at`, `Object.hasOwn`, `String.prototype.replaceAll` and `queueMicrotask`. + `@react-navigation/core` is written against React, so `react` has to resolve to ReactLynx's compat entry, which adds `use`, `useInsertionEffect` and `startTransition`: diff --git a/packages/lynx/src/index.tsx b/packages/lynx/src/index.tsx index 066220a..db0f3f9 100644 --- a/packages/lynx/src/index.tsx +++ b/packages/lynx/src/index.tsx @@ -5,6 +5,8 @@ // The platform layer, mirroring what `@react-navigation/native` does for // React Native: it owns everything Lynx-specific that every navigator needs, // and re-exports core so apps have a single import surface. +import './polyfills'; + export { createStaticNavigation } from './createStaticNavigation'; export { LinkingContext } from './LinkingContext'; export { LocaleDirContext } from './LocaleDirContext'; diff --git a/packages/lynx/src/polyfills.ts b/packages/lynx/src/polyfills.ts new file mode 100644 index 0000000..a091d57 --- /dev/null +++ b/packages/lynx/src/polyfills.ts @@ -0,0 +1,91 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +type Predicate = (value: T, index: number, array: T[]) => boolean; + +function define(target: object, name: string, value: unknown) { + Object.defineProperty(target, name, { + value, + configurable: true, + writable: true, + }); +} + +if (typeof Array.prototype.findLast !== 'function') { + define(Array.prototype, 'findLast', function( + this: T[], + predicate: Predicate, + thisArg?: unknown + ) { + for (let i = this.length - 1; i >= 0; i--) { + if (predicate.call(thisArg, this[i] as T, i, this)) { + return this[i]; + } + } + + return undefined; + }); +} + +if (typeof Array.prototype.findLastIndex !== 'function') { + define(Array.prototype, 'findLastIndex', function( + this: T[], + predicate: Predicate, + thisArg?: unknown + ) { + for (let i = this.length - 1; i >= 0; i--) { + if (predicate.call(thisArg, this[i] as T, i, this)) { + return i; + } + } + + return -1; + }); +} + +if (typeof Array.prototype.at !== 'function') { + define(Array.prototype, 'at', function(this: T[], index: number) { + const i = Math.trunc(index) || 0; + + return this[i < 0 ? this.length + i : i]; + }); +} + +if (typeof Object.hasOwn !== 'function') { + define(Object, 'hasOwn', function(target: object, key: PropertyKey) { + return Object.prototype.hasOwnProperty.call(target, key); + }); +} + +if (typeof String.prototype.replaceAll !== 'function') { + define(String.prototype, 'replaceAll', function( + this: string, + pattern: string | RegExp, + replacement: string | ((...args: string[]) => string) + ) { + if (pattern instanceof RegExp) { + if (!pattern.global) { + throw new TypeError( + 'replaceAll must be called with a global RegExp' + ); + } + + return this.replace(pattern, replacement as string); + } + + const escaped = String(pattern).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + return this.replace(new RegExp(escaped, 'g'), replacement as string); + }); +} + +const globals = globalThis as { + queueMicrotask?: (callback: () => void) => void; +}; + +if (typeof globals.queueMicrotask !== 'function') { + globals.queueMicrotask = (callback: () => void) => { + Promise.resolve().then(callback); + }; +} diff --git a/packages/lynx/src/stack/index.tsx b/packages/lynx/src/stack/index.tsx index 3ccd61e..32f9ab1 100644 --- a/packages/lynx/src/stack/index.tsx +++ b/packages/lynx/src/stack/index.tsx @@ -2,6 +2,8 @@ // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. +import '../polyfills'; + /** * Navigators */