Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lynx-builtin-polyfills.md
Original file line number Diff line number Diff line change
@@ -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`.
5 changes: 5 additions & 0 deletions packages/lynx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
2 changes: 2 additions & 0 deletions packages/lynx/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
91 changes: 91 additions & 0 deletions packages/lynx/src/polyfills.ts
Original file line number Diff line number Diff line change
@@ -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<T> = (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<T>(
this: T[],
predicate: Predicate<T>,
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<T>(
this: T[],
predicate: Predicate<T>,
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<T>(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);
};
}
2 changes: 2 additions & 0 deletions packages/lynx/src/stack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading