From 6cf4e42178ef5608d7e7f1fc0467d312e47cc10c Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 24 Sep 2026 10:27:13 +0200 Subject: [PATCH 1/7] Add public Onyx.get, its read guards, and the ONYX-1 review rule --- .claude/skills/app-coding-standards/SKILL.md | 3 + .../onyx-1-no-render-reachable-onyx-read.md | 181 ++++ config/eslint/eslint.config.mjs | 1 + .../philosophies/ONYX-DATA-MANAGEMENT.md | 22 +- .../no-unsafe-onyx-read.js | 697 ++++++++++++++ package-lock.json | 878 ++++++++++-------- package.json | 36 +- scripts/checkOnyxConnectBypass.ts | 43 +- scripts/onyxConnectBypass.ts | 78 +- src/setup/addUtilsToWindow.ts | 15 - src/types/modules/react-native-onyx.d.ts | 1 - tests/unit/NoUnsafeOnyxReadRuleTest.ts | 446 +++++++++ tests/unit/OnyxConnectBypassTest.ts | 68 +- 13 files changed, 1992 insertions(+), 477 deletions(-) create mode 100644 .claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md create mode 100644 eslint-plugin-local-rules/no-unsafe-onyx-read.js create mode 100644 tests/unit/NoUnsafeOnyxReadRuleTest.ts diff --git a/.claude/skills/app-coding-standards/SKILL.md b/.claude/skills/app-coding-standards/SKILL.md index 376bac8bd283..94054f1cb89b 100644 --- a/.claude/skills/app-coding-standards/SKILL.md +++ b/.claude/skills/app-coding-standards/SKILL.md @@ -76,6 +76,9 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r - [UI-3](rules/ui-3-no-inline-styles.md) — Do not use inline style objects - [UI-4](rules/ui-4-layout-spacing-tokens.md) — Type and responsive insets come from tokens +### Onyx +- [ONYX-1](rules/onyx-1-no-render-reachable-onyx-read.md) — Keep Onyx reads off the render path and out of a written tick + ## Usage **During development**: When writing or modifying `src/` files, consult the relevant standard files for detailed conditions, examples, and exceptions. diff --git a/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md b/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md new file mode 100644 index 000000000000..196ad4f38274 --- /dev/null +++ b/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md @@ -0,0 +1,181 @@ +--- +ruleId: ONYX-1 +title: Keep Onyx reads off the render path and out of a written tick +--- + +## [ONYX-1] Keep Onyx reads off the render path and out of a written tick + +### Reasoning + +`await Onyx.get()` reads a key once and never subscribes. `no-unsafe-onyx-read` catches most misuse, so this rule covers only what lint can't see. + +Do not re-check these: + +| Enforced | By | +|---|---| +| `Onyx.get` outside `src/components`, `src/pages`, `src/hooks` and `tests` | `no-unsafe-onyx-read` | +| A read during render or at module scope | `no-unsafe-onyx-read` | +| A read inside an effect, or in a same-file function an effect calls | `no-unsafe-onyx-read` | +| A Search snapshot key, or a key lint can't resolve | `no-unsafe-onyx-read` | +| A runtime import of `react-native-onyx/dist/OnyxUtils` | `@typescript-eslint/no-restricted-imports` | +| An inline `eslint-disable` of the rule | `scripts/checkOnyxConnectBypass.ts` | +| A missing `await` whose value is then used | `tsc` | + +What's left crosses a file boundary, depends on write ordering, only shows in the diff, or happens after the read. + +Mutating a read result writes the cache, since the value is the cached object. `useOnyx` hands out the same object, so this stays a documented convention and isn't flagged here. + +**A. Position.** Render reaches a read wherever it's written. Lint counts a function as a render body only when it's named like a component or hook or has a top-level `return `, plus `selector` options, lazy initializers and `useSyncExternalStore` snapshots. It misses a helper that a hook calls from render, a helper that returns JSX from inside an `if` or `switch`, and a function passed to a child that calls it during render. + +**B. Tick.** Awaiting a read doesn't wait for an earlier write. `Onyx.get` captures the cached value when it's called, and `merge` and `update` apply to the cache later, so a read queued behind them sees the old value. `set` lands at once today, but don't rely on it: await the write, or read first. A derived key (`ONYXKEYS.DERIVED.*`) is recomputed on a microtask after its source changes, so a read in the same synchronous stretch returns the old derived value, even after a `set` that already updated the source. Read a derived key only before writing its sources. + +**C. Effect in another file.** Lint bans a read inside an effect, but only within one file. A handler that reads can still end up in an effect when it's passed to a child or hook that calls it from `useEffect`, `useLayoutEffect` or `useFocusEffect`. A receiver that only registers the handler for an event (an `on*` prop, `addEventListener`, `useKeyboardShortcut`) is fine, even if the handler sits in that effect's dependency array. + +**D. Output.** A read value that reaches the screen later, through state, a ref or a module variable a component renders, stays frozen at the moment of the read. Flag it when the screen presents it as the current value. + +### Incorrect + +**A1. A hook calls a reading helper from render.** + +```ts +// src/hooks/useCurrentUserEmail.ts +async function getCurrentUserEmail() { + return (await Onyx.get(ONYXKEYS.SESSION))?.email; // fine on its own +} + +function useCurrentUserEmail() { + return getCurrentUserEmail(); // render gets a Promise, and tsc accepts it +} +``` + +**A2. A render body lint doesn't recognize.** + +```tsx +// Every return is JSX, but inside a switch, so lint sees no render body. +async function renderTagBadge(policyID: string) { + const tags = await Onyx.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + + switch (Object.keys(tags ?? {}).length) { + case 0: + return null; + default: + return ; + } +} + + renderTagBadge(item.policyID)} />; +``` + +**B. A read right after a write.** + +```tsx +const onSave = async () => { + Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: true}); + const account = await Onyx.get(ONYXKEYS.ACCOUNT); // isLoading is still the old value +}; +``` + +**C. A child calls the handler from an effect.** + +```tsx +// src/pages/ContactsPage.tsx +function ContactsPage() { + const importContacts = async () => saveContacts(await Onyx.get(ONYXKEYS.COUNTRY_CODE)); + return ; +} + +// src/components/ContactsList.tsx +function ContactsList({onReady}: Props) { + useEffect(() => { + onReady(); // the read now runs inside this effect, and lint only checks one file + }, [onReady]); +} +``` + +**D. A value shown as current, captured at a tap.** + +```tsx +function CurrentTheme() { + const [theme, setTheme] = useState(); + const onPress = async () => setTheme(await Onyx.get(ONYXKEYS.PREFERRED_THEME)); + + // Change the theme in Settings and this still shows the old one. + return Current theme: {theme}; +} +``` + +### Correct + +```tsx +// A: the hook subscribes. +function useCurrentUserEmail() { + const [email] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + return email; +} + +// B: await the write, or read before it. +const onSave = async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: true}); + const account = await Onyx.get(ONYXKEYS.ACCOUNT); +}; + +// B, derived: read the derived key before writing its source. +const onCloseCard = async (cardID: string) => { + const cards = await Onyx.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {state: CONST.EXPENSIFY_CARD.STATE.CLOSED}}); +}; + +// C: keep the subscription in the parent and pass the value down. +const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE); +return saveContacts(countryCode)} />; + +// D: anything shown as current stays on useOnyx. +const [theme] = useOnyx(ONYXKEYS.PREFERRED_THEME); +return Current theme: {theme}; +``` + +--- + +### Review Metadata + +#### A. Position + +- A1. The diff adds a read to a function that some caller reaches from render: a component or hook body, a `useMemo` callback, a `useOnyx` selector, a lazy initializer, an IIFE or array callback in the body, or a local function the body calls. Grep `src/` for the function's name, ignoring imports. A plain-function caller isn't a verdict, so repeat on its name. Comment on the read, naming the calling file and line. +- A2. The function holding the read returns JSX from any branch, or is passed as `renderItem`, `ListHeaderComponent`, or any `render*` or `*Component` prop. Flag the read. +- A3. The diff adds a call at a render position in a component or hook, the call's value is discarded or the callee returns `void`, and the callee's file contains `Onyx.get`. Comment on the call. +- A4. The diff passes a function that reads, or whose file contains `Onyx.get`, as a prop, and the receiver calls that prop from render. Open the receiver's file and Grep the prop's name followed by `(`; follow forwarded props. If the receiver can't be resolved (a spread, or a component held in a variable), ask the author to confirm nothing calls it during render. + +#### B. Tick + +- B1. A write that isn't awaited is followed in the same tick by a call whose file reads the written key, a member of the written collection, or a `DERIVED` key built from it (see `dependencies` in `src/libs/actions/OnyxDerived/configs/`). Repeat on any plain function it calls. Comment on the write, naming the callee and the key. +- B2. The diff adds a read to a function whose callers write that key before calling it in the same tick. + +#### C. Effect in another file + +- C1. The diff passes a function that reads, or whose file contains `Onyx.get`, to another component or hook, or turns a function already passed that way into one that reads. Open the receiver, Grep the prop's name followed by `(`, and flag when a call sits inside a `useEffect`, `useLayoutEffect` or `useFocusEffect` callback, directly or through a local function. Follow forwarded props. Comment on the prop, naming the receiver's effect. + +#### D. Output + +- D1. The read's value goes to a `useState` setter, a `useRef`, or a module variable, a render position in the same file reads it, and the screen presents it as the current value. Comment on the read. + +**DO NOT flag if:** + +- The read sits in an event handler or `useCallback` body that render doesn't invoke, and the reading function isn't exported, isn't a render body by A2, and isn't passed as a render callback +- The prop holding the reader is named `on*` or `handle*`, and every receiver attaches it to an event or calls it from a handler +- The value only reaches a handler argument or a request field and is never rendered +- The receiver only registers the handler for an event (an `on*` prop, `addEventListener`, `useKeyboardShortcut`), even if the handler is in an effect's dependency array +- The removed `useOnyx` value appears nowhere in the diff except the converted call's arguments +- The value is meant as a snapshot of the event, and nothing downstream expects it to update +- The write is awaited, or the read runs in its `.then`, and the read key isn't derived from the written one +- The read sits in a deliberate deferral: a `.then`, a timer, `runAfterTransitions`, `runAfterInteractions`, or a callback passed to an async API. Don't suggest hoisting it above the deferral, since that pins the value to the moment before the wait +- The write and the read are in exclusive branches, or the write's branch returns first +- The keys differ and the read key isn't derived from the written one + +**Search Patterns** (hints for reviewers): + +- `Onyx.get(` +- `Onyx.merge(`, `Onyx.update(`, `Onyx.set(`, `Onyx.mergeCollection(` +- `ONYXKEYS.DERIVED` +- removed `useOnyx(` lines in the diff, then that variable's name in the rest of the diff +- `useEffect(`, `useLayoutEffect(`, `useFocusEffect(`, `useRef(`, `useState(` +- `runAfterTransitions`, `runAfterInteractions`, `.then(`, `setTimeout(` around a read that follows a write diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index 9b6129fdbc7d..45d7ed872457 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -315,6 +315,7 @@ const config = defineConfig([ 'rulesdir/no-layout-spacing-conditional': 'error', 'rulesdir/no-direct-personal-details-list': 'error', 'rulesdir/require-locale-for-localized-date-format': 'error', + 'rulesdir/no-unsafe-onyx-read': 'error', 'rulesdir/prefer-narrow-hook-dependencies': [ 'error', { diff --git a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md index 60c7fb78c89d..1728459102d6 100644 --- a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md +++ b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md @@ -37,10 +37,11 @@ Different platforms come with varying storage capacities and Onyx has a way to g - Add the key to the `evictableKeys` option in `Onyx.init(options)` - A least recently accessed key will only be deleted when an Onyx operation retries after failing. -## Reading Onyx data: `useOnyx` vs `Onyx.connectWithoutView` -There are only two ways to read Onyx data, and `Onyx.connect` is deprecated: +## Reading Onyx data: `useOnyx`, `Onyx.connectWithoutView` and `Onyx.get()` +There are three ways to read Onyx data, and `Onyx.connect` is deprecated: 1. **`useOnyx`** (from `@hooks/useOnyx`) — the default for anything a React component renders. 2. **`Onyx.connectWithoutView`** — an imperative subscription for non-render logic, used only when `useOnyx` genuinely does not fit. +3. **`Onyx.get()`**: an asynchronous, one-shot read of the cache that never subscribes, for event handlers in components, pages and hooks. ### - Prefer a pure function over reading Onyx at all A pure function does not read Onyx itself — it receives the data it needs as parameters, and its caller does the reading (with `useOnyx` or `Onyx.connectWithoutView`) and passes it in. Before adding either subscription, check whether the code can be a pure function instead: it needs no connection, is trivial to test, and cannot cause extra rerenders. Prefer this even when it means passing more arguments. This takes precedence over everything below. @@ -60,6 +61,23 @@ Add an inline comment at each new `Onyx.connectWithoutView` call stating why the ### - Using `Onyx.connectWithoutView` in a component for performance REQUIRES @frontend-performance approval In rare cases a component that subscribes to multiple large collections through `useOnyx` suffers a significant performance regression. Reaching for `Onyx.connectWithoutView` to avoid that is an explicit exception, not a self-serve option: it MUST be approved by the `@frontend-performance` team on Slack, and the PR description MUST link to that discussion. +### - `Onyx.get()` is ONLY for event handlers in components, pages and hooks +It reads the cache once and never subscribes, so the value it returns MUST NOT reach rendered output, directly or through state, a ref or a module variable. Use it in event handlers and `useCallback` bodies under `src/components`, `src/pages` and `src/hooks`. Never during render, at module scope, or in code an effect runs. + +### - `Onyx.get()` MUST NOT read the Search snapshot keys +`@hooks/useOnyx` redirects the keys in `CONST.SEARCH.SNAPSHOT_ONYX_KEYS` to a Search snapshot inside a `SearchScopeProvider`, and `Onyx.get()` always reads the global key. These keys stay on `useOnyx`. + +### - Reads MUST come before a write in the same tick, or after the write is awaited +`Onyx.get()` captures the cache when it is called, and most writes land later, so a read queued behind a write returns the old value. A derived key (`ONYXKEYS.DERIVED.*`) lags its sources, so read it only before writing them. + +### - A subscription that triggers work MUST stay on `useOnyx` +If the value re-runs an effect, directly or through a callback in a dependency array, a one-shot read stops that effect from re-running. + +### - Reapply the `selector` and never mutate the result +`Onyx.get()` returns the stored value, not the `selector` projection `useOnyx` hands out, and a single-key read is the cached object itself, so writing to it changes the cache without telling subscribers. + +`rulesdir/no-unsafe-onyx-read` enforces the mechanical parts of these rules and cannot be disabled inline. [ONYX-1](../../.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md) covers the rest in review, with examples. + ## Onyx Derived Values Derived values are special Onyx keys which contain values derived from other Onyx values. These are available as a performance optimization, so that if the result of a common computation of Onyx values is needed in many places across the app, the computation can be done only as needed in a centralized location, and then shared across the app. Once created, Onyx derived values are stored and consumed just like any other Onyx value. diff --git a/eslint-plugin-local-rules/no-unsafe-onyx-read.js b/eslint-plugin-local-rules/no-unsafe-onyx-read.js new file mode 100644 index 000000000000..fb099ca86874 --- /dev/null +++ b/eslint-plugin-local-rules/no-unsafe-onyx-read.js @@ -0,0 +1,697 @@ +import fs from 'fs'; +import path from 'path'; + +const name = 'no-unsafe-onyx-read'; + +const ONYX_MODULE = 'react-native-onyx'; + +const SNAPSHOT_KEYS_SOURCE = 'src/CONST/runtimeConfigured.ts'; + +const SNAPSHOT_KEYS_DECLARATION = /SEARCH_SNAPSHOT_ONYX_KEYS:\s*\[([^\]]*)\]/; + +const ONYXKEYS_ROOT = 'ONYXKEYS'; + +function findRepoRoot() { + let current = path.resolve(process.cwd()); + + while (true) { + if (fs.existsSync(path.join(current, SNAPSHOT_KEYS_SOURCE))) { + return current; + } + + const parent = path.dirname(current); + + if (parent === current) { + return null; + } + + current = parent; + } +} + +const REPO_ROOT = findRepoRoot(); + +function resolveRestrictedKeyPaths() { + const repoRoot = REPO_ROOT; + + if (!repoRoot) { + throw new Error(`no-unsafe-onyx-read could not locate ${SNAPSHOT_KEYS_SOURCE}. Without it the rule would silently stop refusing Search snapshot keys.`); + } + + const source = fs.readFileSync(path.join(repoRoot, SNAPSHOT_KEYS_SOURCE), 'utf8'); + const declaration = SNAPSHOT_KEYS_DECLARATION.exec(source); + + if (!declaration) { + throw new Error(`no-unsafe-onyx-read could not read SEARCH_SNAPSHOT_ONYX_KEYS from ${SNAPSHOT_KEYS_SOURCE}. Without it the rule would silently stop refusing Search snapshot keys.`); + } + + return new Set([...declaration[1].matchAll(/ONYXKEYS\.([A-Z0-9_.]+)/g)].map((match) => match[1])); +} + +const RESTRICTED_KEY_PATHS = resolveRestrictedKeyPaths(); + +const READ_METHOD = 'get'; + +const READ_ALLOWED_DIRECTORIES = ['src/components/', 'src/pages/', 'src/hooks/', 'tests/']; + +const READ_ALLOWED_FILES = new Set([]); + +const EFFECT_HOOK_NAMES = new Set(['useEffect', 'useLayoutEffect', 'useInsertionEffect', 'useFocusEffect']); + +const CALLBACK_HOOK_NAMES = new Set(['useCallback']); + +const EFFECT_CONTINUATION_NAMES = new Set(['then', 'catch', 'finally', 'setTimeout', 'requestAnimationFrame', 'queueMicrotask', 'runAfterInteractions', 'runAfterTransitions']); + +const TYPE_ONLY_EXPRESSIONS = new Set(['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression', 'TSInstantiationExpression', 'TSTypeAssertion']); + +const SYNCHRONOUS_CALLBACK_METHODS = new Set(['map', 'filter', 'reduce', 'reduceRight', 'forEach', 'find', 'findIndex', 'findLast', 'findLastIndex', 'flatMap', 'some', 'every', 'sort']); + +const RENDER_TIME_HOOK_ARGUMENTS = new Map([ + ['useMemo', new Set([0])], + ['useState', new Set([0])], + ['useReducer', new Set([2])], + ['useSyncExternalStore', new Set([1, 2])], +]); + +const RENDER_TIME_OPTION_NAMES = new Set(['selector']); + +const COMPONENT_WRAPPER_NAMES = new Set(['memo', 'forwardRef']); + +const SYNCHRONOUS_EXECUTOR_NAMES = new Set(['Promise']); + +const RENDER = 'render'; +const DEFERRED = 'deferred'; +const SYNCHRONOUS = 'synchronous'; + +const MODULE_SCOPE = 'moduleScope'; +const EVENT = 'event'; +const EFFECT = 'effect'; + +const meta = { + type: 'problem', + docs: { + description: 'Disallow unsafe Onyx reads: Onyx.get outside components, pages, hooks and tests, during render, inside effects, at module scope or on Search snapshot keys.', + recommended: 'error', + }, + schema: [], + messages: { + noOnyxGetInRender: + 'Do not read Onyx during render. Onyx.get() is a one-shot read that never subscribes, so a value obtained while rendering does not re-render the component when that key changes and the UI can show stale data indefinitely. A component cannot await it either, so reaching it from render means use() or .then(), both of which read without subscribing.\n\n' + + 'Use useOnyx() for anything the component renders. Reserve Onyx.get() for code that runs on an event: event handlers and useCallback bodies.', + noOnyxReadAtModuleScope: + 'Do not read Onyx at module scope. A module body runs at import time and cannot await, so the value can only be parked in a module variable through .then(), where it is a one-shot snapshot that never updates when the key changes.\n\n' + + 'Move the read inside the function that needs it, so it runs at event time and reads the current value. If the module genuinely needs to track a key, subscribe with Onyx.connectWithoutView() instead of caching one read.', + noUnresolvableOnyxKey: + 'Do not read Onyx with a key this rule cannot resolve. The read surface is restricted to keys that are provably not Search snapshot keys, and a key built at runtime cannot be checked, so a caller can route a snapshot key here without anything failing.\n\n' + + 'Write the key as an ONYXKEYS access, such as ONYXKEYS.SESSION, or as a template literal that starts with an ONYXKEYS collection prefix. If the key cannot be static, keep the useOnyx subscription or take the value as a parameter. An inline eslint-disable of this rule fails the lint run.', + noRestrictedOnyxKey: + 'Do not read {{keyPath}} with a one-shot Onyx read. src/hooks/useOnyx.ts rewrites this key to snapshot_ inside a SearchScopeProvider subtree, so a component subscribed to it may never have been reading the global key at all. A read here returns live data where the component saw the snapshot, and nothing at the call site can tell the two apart.\n\n' + + 'Take the value as a parameter from the component, which knows whether it is inside a Search scope, or keep the useOnyx subscription.', + noOnyxReadOutsideAllowedPath: + 'Onyx.get() is only allowed in src/components, src/pages, src/hooks and tests.\n\n' + + 'Elsewhere, take the value as a parameter or keep the Onyx.connectWithoutView() subscription. A file joins READ_ALLOWED_FILES only in a PR that removes an Onyx.connectWithoutView() from it.', + noOnyxReadInEffect: + 'Do not read Onyx inside an effect, or in a function an effect calls. When the value was in the effect dependency array, the useOnyx subscription is what re-runs the effect, and a one-shot read stops that.\n\n' + + 'Keep the useOnyx subscription. Reads inside effects stay banned until a check can confirm the value was not in the dependency array.', + }, +}; + +function isFunctionNode(node) { + return node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression'; +} + +function isHookName(functionName) { + return /^use[A-Z0-9]/.test(functionName); +} + +function isComponentName(functionName) { + return /^[A-Z]/.test(functionName); +} + +function getFunctionName(functionNode, parent) { + if (functionNode.id?.type === 'Identifier') { + return functionNode.id.name; + } + + if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') { + return parent.id.name; + } + + if (parent?.type === 'Property' && !parent.computed && parent.key?.type === 'Identifier') { + return parent.key.name; + } + + return null; +} + +function returnsJSX(functionNode) { + const body = functionNode.body; + + if (!body) { + return false; + } + + if (body.type === 'JSXElement' || body.type === 'JSXFragment') { + return true; + } + + if (body.type !== 'BlockStatement') { + return false; + } + + return body.body.some((statement) => statement.type === 'ReturnStatement' && (statement.argument?.type === 'JSXElement' || statement.argument?.type === 'JSXFragment')); +} + +function getStaticName(keyNode, computed) { + if (!computed && keyNode.type === 'Identifier') { + return keyNode.name; + } + + if (keyNode.type === 'Literal' && typeof keyNode.value === 'string') { + return keyNode.value; + } + + return null; +} + +function getStaticPropertyName(memberExpression) { + return getStaticName(memberExpression.property, memberExpression.computed); +} + +function unwrapKeyExpression(node) { + if (node && TYPE_ONLY_EXPRESSIONS.has(node.type)) { + return unwrapKeyExpression(node.expression); + } + + if (node?.type !== 'TemplateLiteral') { + return node; + } + + const leadingExpression = node.expressions.at(0); + + // cspell:disable-next-line -- quasis is the ESTree name for the static chunks of a template literal + if (!leadingExpression || node.quasis.at(0)?.value.cooked !== '') { + return node; + } + + return unwrapKeyExpression(leadingExpression); +} + +function getVariableByName(scope, variableName) { + let currentScope = scope; + + while (currentScope) { + const variable = currentScope.variables.find((scopeVariable) => scopeVariable.name === variableName); + + if (variable) { + return variable; + } + + currentScope = currentScope.upper; + } + + return null; +} + +function getConstInitializer(node, scope) { + const variable = getVariableByName(scope, node.name); + + if (variable?.defs.length !== 1) { + return null; + } + + const definition = variable.defs.at(0); + + if (definition.type !== 'Variable' || definition.parent?.kind !== 'const' || definition.node.id.type !== 'Identifier') { + return null; + } + + return definition.node.init ?? null; +} + +function getOnyxKeyPath(node, scope, seen = new Set()) { + const segments = []; + let current = unwrapKeyExpression(node); + + if (current?.type === 'Identifier' && current.name !== ONYXKEYS_ROOT) { + if (seen.has(current)) { + return null; + } + + seen.add(current); + const initializer = getConstInitializer(current, scope); + + return initializer ? getOnyxKeyPath(initializer, scope, seen) : null; + } + + while (current?.type === 'MemberExpression') { + const propertyName = getStaticPropertyName(current); + + if (!propertyName) { + return null; + } + + segments.unshift(propertyName); + current = current.object; + } + + if (current?.type !== 'Identifier' || current.name !== ONYXKEYS_ROOT || segments.length === 0) { + return null; + } + + return segments.join('.'); +} + +function getCalleeName(callee) { + if (callee.type === 'Identifier') { + return callee.name; + } + + return callee.type === 'MemberExpression' ? getStaticPropertyName(callee) : null; +} + +function matchesCalleeName(callee, names) { + const calleeName = getCalleeName(callee); + + return !!calleeName && names.has(calleeName); +} + +function getRenderTimeArgumentIndices(callee) { + const calleeName = getCalleeName(callee); + + return calleeName ? (RENDER_TIME_HOOK_ARGUMENTS.get(calleeName) ?? null) : null; +} + +function isHookOption(property) { + const call = property.parent?.parent; + + if (call?.type !== 'CallExpression' || !call.arguments.includes(property.parent)) { + return false; + } + + const calleeName = getCalleeName(call.callee); + + return !!calleeName && isHookName(calleeName); +} + +function isOnyxModuleSource(sourceValue) { + return sourceValue === ONYX_MODULE; +} + +function isRenderTimeUsage(identifier) { + const parent = identifier.parent; + + if (parent?.type === 'Property' && parent.value === identifier && RENDER_TIME_OPTION_NAMES.has(getStaticName(parent.key, parent.computed)) && isHookOption(parent)) { + return true; + } + + if (parent?.type !== 'CallExpression' || !parent.arguments.includes(identifier)) { + return false; + } + + return matchesCalleeName(parent.callee, COMPONENT_WRAPPER_NAMES) || !!getRenderTimeArgumentIndices(parent.callee)?.has(parent.arguments.indexOf(identifier)); +} + +function getFunctionBinding(functionNode, parent) { + if (functionNode.type === 'FunctionDeclaration' && functionNode.id?.type === 'Identifier') { + return {declaration: functionNode, name: functionNode.id.name}; + } + + if (parent?.type === 'VariableDeclarator' && parent.init === functionNode && parent.id.type === 'Identifier') { + return {declaration: parent, name: parent.id.name}; + } + + return null; +} + +function isReferencedAtRenderTime(declaration, boundName, sourceCode, seen = new Set()) { + const variable = sourceCode.getDeclaredVariables(declaration).find((declaredVariable) => declaredVariable.name === boundName); + + if (!variable || seen.has(variable)) { + return false; + } + + seen.add(variable); + + return variable.references.some((reference) => { + const identifier = reference.identifier; + + if (isRenderTimeUsage(identifier)) { + return true; + } + + const parent = identifier.parent; + + if (parent?.type !== 'VariableDeclarator' || parent.init !== identifier || parent.id.type !== 'Identifier') { + return false; + } + + return isReferencedAtRenderTime(parent, parent.id.name, sourceCode, seen); + }); +} + +function isEffectCallback(functionNode, parent, grandparent) { + if (parent?.type !== 'CallExpression' || parent.arguments.at(0) !== functionNode) { + return false; + } + + if (matchesCalleeName(parent.callee, EFFECT_HOOK_NAMES)) { + return true; + } + + return ( + matchesCalleeName(parent.callee, CALLBACK_HOOK_NAMES) && + grandparent?.type === 'CallExpression' && + grandparent.arguments.at(0) === parent && + matchesCalleeName(grandparent.callee, EFFECT_HOOK_NAMES) + ); +} + +function getHandlerBinding(functionNode, parent, grandparent) { + const binding = getFunctionBinding(functionNode, parent); + + if (binding) { + return binding; + } + + if ( + parent?.type === 'CallExpression' && + parent.arguments.at(0) === functionNode && + matchesCalleeName(parent.callee, CALLBACK_HOOK_NAMES) && + grandparent?.type === 'VariableDeclarator' && + grandparent.init === parent && + grandparent.id.type === 'Identifier' + ) { + return {declaration: grandparent, name: grandparent.id.name}; + } + + return null; +} + +function runsWithEnclosingCode(functionNode, parent) { + if (parent?.type === 'NewExpression' && matchesCalleeName(parent.callee, SYNCHRONOUS_EXECUTOR_NAMES)) { + return true; + } + + if (parent?.type !== 'CallExpression') { + return false; + } + + if (parent.callee === functionNode) { + return true; + } + + return ( + parent.arguments.includes(functionNode) && + (matchesCalleeName(parent.callee, EFFECT_CONTINUATION_NAMES) || (parent.callee.type === 'MemberExpression' && matchesCalleeName(parent.callee, SYNCHRONOUS_CALLBACK_METHODS))) + ); +} + +function isReachedFromEffect(ancestors, fromIndex, sourceCode, seen) { + function isHandlerInvokedFromEffect(binding) { + const variable = sourceCode.getDeclaredVariables(binding.declaration).find((declaredVariable) => declaredVariable.name === binding.name); + + if (!variable || seen.has(variable)) { + return false; + } + + seen.add(variable); + + return variable.references.some((reference) => { + if (!reference.isRead()) { + return false; + } + + const identifier = reference.identifier; + const parent = identifier.parent; + + if (parent?.type === 'CallExpression' && parent.arguments.at(0) === identifier && isEffectCallback(identifier, parent, parent.parent)) { + return true; + } + + const isCalled = parent?.type === 'CallExpression' && parent.callee === identifier; + + if (!isCalled && !runsWithEnclosingCode(identifier, parent)) { + return false; + } + + const referenceAncestors = sourceCode.getAncestors(identifier); + + return isReachedFromEffect(referenceAncestors, referenceAncestors.length - 1, sourceCode, seen); + }); + } + + for (let index = fromIndex; index >= 0; index--) { + const ancestor = ancestors[index]; + + if (!isFunctionNode(ancestor)) { + continue; + } + + const parent = ancestors[index - 1] ?? null; + const grandparent = ancestors[index - 2] ?? null; + + if (isEffectCallback(ancestor, parent, grandparent)) { + return true; + } + + const binding = getHandlerBinding(ancestor, parent, grandparent); + + if (binding) { + return isHandlerInvokedFromEffect(binding); + } + + if (!runsWithEnclosingCode(ancestor, parent)) { + return false; + } + } + + return false; +} + +function isReadAllowedInFile(filename) { + if (!REPO_ROOT || !filename || !path.isAbsolute(filename)) { + return true; + } + + const relativePath = path.relative(REPO_ROOT, filename).split(path.sep).join('/'); + + if (relativePath.startsWith('..')) { + return true; + } + + return READ_ALLOWED_DIRECTORIES.some((directory) => relativePath.startsWith(directory)) || READ_ALLOWED_FILES.has(relativePath); +} + +function classifyFunctionBoundary(functionNode, parent, sourceCode) { + if (parent?.type === 'Property' && parent.value === functionNode && RENDER_TIME_OPTION_NAMES.has(getStaticName(parent.key, parent.computed)) && isHookOption(parent)) { + return RENDER; + } + + if (parent?.type === 'NewExpression' && parent.arguments.at(0) === functionNode && matchesCalleeName(parent.callee, SYNCHRONOUS_EXECUTOR_NAMES)) { + return SYNCHRONOUS; + } + + if (parent?.type === 'CallExpression') { + if (parent.callee === functionNode) { + return SYNCHRONOUS; + } + + if (parent.arguments.includes(functionNode)) { + if (matchesCalleeName(parent.callee, COMPONENT_WRAPPER_NAMES)) { + return RENDER; + } + + if (getRenderTimeArgumentIndices(parent.callee)?.has(parent.arguments.indexOf(functionNode))) { + return RENDER; + } + + if (parent.callee.type === 'MemberExpression' && matchesCalleeName(parent.callee, SYNCHRONOUS_CALLBACK_METHODS)) { + return SYNCHRONOUS; + } + + return DEFERRED; + } + } + + const binding = getFunctionBinding(functionNode, parent); + + if (binding && isReferencedAtRenderTime(binding.declaration, binding.name, sourceCode)) { + return RENDER; + } + + const functionName = getFunctionName(functionNode, parent); + + if (functionName && (isHookName(functionName) || isComponentName(functionName))) { + return RENDER; + } + + return returnsJSX(functionNode) ? RENDER : DEFERRED; +} + +function classifyPosition(ancestors, sourceCode) { + let sawJSXExpression = false; + + for (let index = ancestors.length - 1; index >= 0; index--) { + const ancestor = ancestors[index]; + + if (ancestor.type === 'JSXExpressionContainer') { + sawJSXExpression = true; + continue; + } + + if (!isFunctionNode(ancestor)) { + continue; + } + + if (sawJSXExpression) { + return RENDER; + } + + const disposition = classifyFunctionBoundary(ancestor, ancestors[index - 1] ?? null, sourceCode); + + if (disposition === DEFERRED) { + return isReachedFromEffect(ancestors, index, sourceCode, new Set()) ? EFFECT : EVENT; + } + + if (disposition === RENDER) { + return RENDER; + } + } + + return MODULE_SCOPE; +} + +function findRestrictedKey(keyArgument, scope) { + const keyPath = getOnyxKeyPath(keyArgument, scope); + + if (!keyPath) { + return {keyPath: null}; + } + + return RESTRICTED_KEY_PATHS.has(keyPath) ? {keyPath} : null; +} + +function create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + const filename = context.filename ?? context.getFilename(); + const onyxImportBindings = new WeakSet(); + const readAliases = new WeakSet(); + + function trackBinding(node, bindingName, bindings) { + const variable = sourceCode.getDeclaredVariables(node).find((declaredVariable) => declaredVariable.name === bindingName); + + if (variable) { + bindings.add(variable); + } + + return variable; + } + + function isOnyxRead(node, scope) { + if (node?.type !== 'MemberExpression' || node.object.type !== 'Identifier') { + return false; + } + + if (getStaticPropertyName(node) !== READ_METHOD) { + return false; + } + + const objectVariable = getVariableByName(scope, node.object.name); + return !!objectVariable && onyxImportBindings.has(objectVariable); + } + + return { + ImportDeclaration(node) { + if (!isOnyxModuleSource(node.source.value)) { + return; + } + + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier') { + trackBinding(node, specifier.local.name, onyxImportBindings); + } + } + }, + VariableDeclarator(node) { + const scope = sourceCode.getScope(node); + + if (node.id.type === 'ObjectPattern' && node.init?.type === 'Identifier') { + const initVariable = getVariableByName(scope, node.init.name); + + if (!initVariable || !onyxImportBindings.has(initVariable)) { + return; + } + + for (const property of node.id.properties) { + if (property.type !== 'Property' || property.value.type !== 'Identifier') { + continue; + } + + const keyName = getStaticName(property.key, property.computed); + + if (keyName === READ_METHOD) { + trackBinding(node, property.value.name, readAliases); + } + } + return; + } + + if (node.id.type !== 'Identifier') { + return; + } + + if (node.init?.type === 'Identifier') { + const aliasedVariable = getVariableByName(scope, node.init.name); + + if (aliasedVariable && onyxImportBindings.has(aliasedVariable)) { + trackBinding(node, node.id.name, onyxImportBindings); + } + } + + if (isOnyxRead(node.init, scope)) { + trackBinding(node, node.id.name, readAliases); + } + }, + CallExpression(node) { + const scope = sourceCode.getScope(node); + const calleeVariable = node.callee.type === 'Identifier' ? getVariableByName(scope, node.callee.name) : null; + + if (!isOnyxRead(node.callee, scope) && !(!!calleeVariable && readAliases.has(calleeVariable))) { + return; + } + + if (!isReadAllowedInFile(filename)) { + context.report({node, messageId: 'noOnyxReadOutsideAllowedPath'}); + return; + } + + const position = classifyPosition(sourceCode.getAncestors(node), sourceCode); + + if (position === MODULE_SCOPE) { + context.report({node, messageId: 'noOnyxReadAtModuleScope'}); + return; + } + + if (position === RENDER) { + context.report({node, messageId: 'noOnyxGetInRender'}); + return; + } + + if (position === EFFECT) { + context.report({node, messageId: 'noOnyxReadInEffect'}); + return; + } + + const finding = findRestrictedKey(node.arguments.at(0), scope); + + if (finding) { + context.report( + finding.keyPath ? {node, messageId: 'noRestrictedOnyxKey', data: {keyPath: `${ONYXKEYS_ROOT}.${finding.keyPath}`}} : {node, messageId: 'noUnresolvableOnyxKey'}, + ); + } + }, + }; +} + +export {name, meta, create}; diff --git a/package-lock.json b/package-lock.json index 3eb37d7e7423..0771deafb003 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.5.5-0", + "version": "9.4.92-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.5.5-0", + "version": "9.4.92-0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -21,7 +21,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.342", + "@expensify/react-native-live-markdown": "0.1.336", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", @@ -62,13 +62,12 @@ "array.prototype.tosorted": "^1.1.4", "awesome-phonenumber": "^5.4.0", "canvas-size": "^1.2.6", - "canvaskit-wasm": "0.41.0", "d3-scale": "^4.0.2", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.207", + "expensify-common": "2.0.201", "expo": "57.0.8", "expo-asset": "57.0.7", "expo-audio": "57.0.3", @@ -90,11 +89,11 @@ "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", "json5": "2.2.2", - "lodash-es": "4.18.1", + "lodash-es": "4.17.21", "lottie-react-native": "7.3.8", "mapbox-gl": "^3.24.0", - "metro": "0.84.5", - "metro-transform-plugins": "0.84.5", + "metro": "0.84.4", + "metro-transform-plugins": "0.84.4", "onfido-sdk-ui": "14.53.1", "pako": "^2.1.0", "process": "^0.11.10", @@ -116,7 +115,7 @@ "react-native-device-info": "10.3.1", "react-native-draggable-flatlist": "^4.0.3", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "3.3.0", + "react-native-gesture-handler": "2.32.0", "react-native-google-places-autocomplete": "2.6.4", "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.1.2", @@ -126,8 +125,8 @@ "react-native-localize": "^3.5.4", "react-native-nitro-fetch": "1.6.2", "react-native-nitro-modules": "0.37.1", - "react-native-nitro-sqlite": "9.8.3", - "react-native-onyx": "3.0.116", + "react-native-nitro-sqlite": "9.6.0", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b3c781503af7ba879149c028cc0c36523e8a7259", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -138,8 +137,8 @@ "react-native-quick-crypto": "^1.1.6", "react-native-reanimated": "4.5.5", "react-native-render-html": "6.3.1", - "react-native-safe-area-context": "5.9.1", - "react-native-screens": "4.28.0", + "react-native-safe-area-context": "5.6.2", + "react-native-screens": "4.25.0", "react-native-share": "11.0.2", "react-native-svg": "15.15.5", "react-native-tab-view": "^4.3.0", @@ -154,7 +153,7 @@ "react-webcam": "^7.1.1", "scheduler": "0.27.0", "victory-native": "^41.21.0", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" + "xlsx": "^0.18.5" }, "devDependencies": { "@aaroon/workbox-rspack-plugin": "^1.0.1", @@ -2913,14 +2912,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.4", + "version": "7.25.9", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -4354,6 +4353,22 @@ "node": ">=8" } }, + "node_modules/@callstack/repack/node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/@callstack/repack/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -5204,6 +5219,16 @@ "url": "https://github.com/sponsors/dword-design" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", @@ -5857,20 +5882,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5936,16 +5951,19 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.342", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.342.tgz", - "integrity": "sha512-xt8t0pUTHHPfY9G25wZ0rQUP44eFdVQp44Mo2Ji2H66zr/qjUnDqDOQPJxi5azucUwI/uORmmMJOvXMdKsEDNg==", + "version": "0.1.336", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.336.tgz", + "integrity": "sha512-rrbcQmiTNdv7iySVbQWoY7wMGpIf9e+Wx7LJkx23AGkGwAkq7Mfptsly/4a6GaunIQ3Qh5+D84cLxlI42rLlWQ==", "license": "MIT", "workspaces": [ "./example", "./WebExample" ], + "engines": { + "node": ">= 18.0.0" + }, "peerDependencies": { - "expensify-common": ">=2.0.207", + "expensify-common": ">=2.0.189", "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" @@ -7478,25 +7496,25 @@ } }, "node_modules/@expo/metro": { - "version": "56.0.2", - "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.2.tgz", - "integrity": "sha512-Ld5AeYMCCDa8bLeWhfuLbZFFjlV3f6ORqyPz2glGh6RltIngMuLf9BTC2yvHFjkKuGxL5SynijmA8xmNNWn5iA==", - "license": "MIT", - "dependencies": { - "metro": "0.84.5", - "metro-babel-transformer": "0.84.5", - "metro-cache": "0.84.5", - "metro-cache-key": "0.84.5", - "metro-config": "0.84.5", - "metro-core": "0.84.5", - "metro-file-map": "0.84.5", - "metro-minify-terser": "0.84.5", - "metro-resolver": "0.84.5", - "metro-runtime": "0.84.5", - "metro-source-map": "0.84.5", - "metro-symbolicate": "0.84.5", - "metro-transform-plugins": "0.84.5", - "metro-transform-worker": "0.84.5" + "version": "56.0.0", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.0.tgz", + "integrity": "sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A==", + "license": "MIT", + "dependencies": { + "metro": "0.84.4", + "metro-babel-transformer": "0.84.4", + "metro-cache": "0.84.4", + "metro-cache-key": "0.84.4", + "metro-config": "0.84.4", + "metro-core": "0.84.4", + "metro-file-map": "0.84.4", + "metro-minify-terser": "0.84.4", + "metro-resolver": "0.84.4", + "metro-runtime": "0.84.4", + "metro-source-map": "0.84.4", + "metro-symbolicate": "0.84.4", + "metro-transform-plugins": "0.84.4", + "metro-transform-worker": "0.84.4" } }, "node_modules/@expo/metro-config": { @@ -8489,9 +8507,9 @@ } }, "node_modules/@expo/xcpretty/node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -12510,19 +12528,6 @@ "eslint-scope": "5.1.1" } }, - "node_modules/@nodable/entities": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", - "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "devOptional": true, @@ -14365,24 +14370,29 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.5", + "version": "2.0.4", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", + "version": "1.1.0", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.1", + "version": "1.1.0", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, "node_modules/@protobufjs/float": { "version": "1.0.2", "license": "BSD-3-Clause" }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "license": "BSD-3-Clause" + }, "node_modules/@protobufjs/path": { "version": "1.1.2", "license": "BSD-3-Clause" @@ -14392,7 +14402,7 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.1", + "version": "1.1.0", "license": "BSD-3-Clause" }, "node_modules/@pusher/pusher-websocket-react-native": { @@ -14555,20 +14565,10 @@ } }, "node_modules/@react-native-community/cli-config/node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -16126,9 +16126,9 @@ } }, "node_modules/@rsbuild/plugin-svgr/node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -18230,23 +18230,6 @@ "devOptional": true, "license": "BSD-3-Clause" }, - "node_modules/@simple-git/args-pathspec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.4.tgz", - "integrity": "sha512-EtMX6XjRWSastG2SdkmSQByPtJ9dx/NIjnHbpQPCYt62j4RmSx5rgLTGpw0YCjF5h191gZOmBfheOT23cRSFdw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-git/args-pathspec": "^1.0.3" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "license": "MIT" @@ -19985,6 +19968,10 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.41", + "license": "MIT" + }, "node_modules/@types/howler": { "version": "2.2.12", "dev": true, @@ -21577,14 +21564,23 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/adm-zip": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", - "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.0" + "node": ">=12.0" } }, "node_modules/agent-base": { @@ -21741,19 +21737,6 @@ "node": ">= 8" } }, - "node_modules/anynum": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", - "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/app-root-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", @@ -22461,9 +22444,7 @@ } }, "node_modules/babel-plugin-module-resolver/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "version": "5.1.6", "dev": true, "license": "ISC", "dependencies": { @@ -22717,9 +22698,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.27", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", - "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -22936,9 +22917,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -22955,11 +22936,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -22988,6 +22969,40 @@ "browserslist-to-es-version": "dist/cli.js" } }, + "node_modules/browserslist-to-es-version/node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bser": { "version": "2.1.1", "license": "Apache-2.0", @@ -23212,9 +23227,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001814", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", - "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", "funding": [ { "type": "opencollective", @@ -23254,6 +23269,19 @@ "node": ">=4" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -23642,6 +23670,15 @@ "dev": true, "license": "MIT" }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -23929,6 +23966,19 @@ "node": ">=8" } }, + "node_modules/concurrently/node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/concurrently/node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -24051,6 +24101,39 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-js-compat/node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -24077,6 +24160,16 @@ "node": ">= 0.4.0" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -24342,9 +24435,9 @@ } }, "node_modules/cspell-glob/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -25372,9 +25465,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.444", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", - "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", "license": "ISC" }, "node_modules/emittery": { @@ -26874,9 +26967,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.207", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.207.tgz", - "integrity": "sha512-AHrgp9LZkS9z/37G5ZbKbIwa9GqSCPVwfwilS1yhSQmFyL6Jb/eDxlopMfthaTbXO0NHTPRH2UmImlUtdlNYvw==", + "version": "2.0.201", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.201.tgz", + "integrity": "sha512-8ziun4VC5bcQobhb+rmpCuaAiXBapPrxiAUyITt6lLaYBa1K2pfkcvZypgCmrj3eLselOvAVkf5Y/dVDcWs6Cg==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", @@ -26896,9 +26989,9 @@ } }, "node_modules/expensify-common/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -27690,42 +27783,22 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-builder": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", - "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.6.2", - "xml-naming": "^0.3.0" - } - }, "node_modules/fast-xml-parser": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", - "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", + "version": "4.4.1", "devOptional": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", "dependencies": { - "@nodable/entities": "^3.0.0", - "fast-xml-builder": "^1.2.0", - "is-unsafe": "^2.0.0", - "path-expression-matcher": "^1.6.2", - "strnum": "^2.4.2", - "xml-naming": "^0.3.0" + "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" @@ -28225,9 +28298,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -28385,17 +28458,17 @@ } }, "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" @@ -28441,6 +28514,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -28882,12 +28964,10 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "9.0.5", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -29193,9 +29273,7 @@ } }, "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "version": "2.0.2", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -29239,6 +29317,17 @@ "hermes-estree": "0.36.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -29608,22 +29697,20 @@ } }, "node_modules/image-size": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.4.tgz", - "integrity": "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw==", - "dev": true, + "version": "1.1.1", "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, "bin": { "image-size": "bin/image-size.js" }, "engines": { - "node": ">=18" + "node": ">=16.x" } }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/import-fresh": { @@ -30260,19 +30347,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", - "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -32781,9 +32855,7 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", - "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "version": "3.14.1", "devOptional": true, "license": "MIT", "dependencies": { @@ -33327,6 +33399,19 @@ "shell-quote": "^1.8.4" } }, + "node_modules/launch-editor/node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/lefthook": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.1.9.tgz", @@ -33881,9 +33966,7 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "version": "4.17.21", "license": "MIT" }, "node_modules/lodash.bindall": { @@ -34401,9 +34484,9 @@ } }, "node_modules/metro": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.5.tgz", - "integrity": "sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.4.tgz", + "integrity": "sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -34421,22 +34504,23 @@ "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", + "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.84.5", - "metro-cache": "0.84.5", - "metro-cache-key": "0.84.5", - "metro-config": "0.84.5", - "metro-core": "0.84.5", - "metro-file-map": "0.84.5", - "metro-resolver": "0.84.5", - "metro-runtime": "0.84.5", - "metro-source-map": "0.84.5", - "metro-symbolicate": "0.84.5", - "metro-transform-plugins": "0.84.5", - "metro-transform-worker": "0.84.5", + "metro-babel-transformer": "0.84.4", + "metro-cache": "0.84.4", + "metro-cache-key": "0.84.4", + "metro-config": "0.84.4", + "metro-core": "0.84.4", + "metro-file-map": "0.84.4", + "metro-resolver": "0.84.4", + "metro-runtime": "0.84.4", + "metro-source-map": "0.84.4", + "metro-symbolicate": "0.84.4", + "metro-transform-plugins": "0.84.4", + "metro-transform-worker": "0.84.4", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", @@ -34453,15 +34537,15 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.5.tgz", - "integrity": "sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.4.tgz", + "integrity": "sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", - "metro-cache-key": "0.84.5", + "metro-cache-key": "0.84.4", "nullthrows": "^1.1.1" }, "engines": { @@ -34484,24 +34568,24 @@ } }, "node_modules/metro-cache": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.5.tgz", - "integrity": "sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.4.tgz", + "integrity": "sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==", "license": "MIT", "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", - "metro-core": "0.84.5" + "metro-core": "0.84.4" }, "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-cache-key": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.5.tgz", - "integrity": "sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.4.tgz", + "integrity": "sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -34533,18 +34617,18 @@ } }, "node_modules/metro-config": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.5.tgz", - "integrity": "sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.4.tgz", + "integrity": "sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==", "license": "MIT", "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", - "metro": "0.84.5", - "metro-cache": "0.84.5", - "metro-core": "0.84.5", - "metro-runtime": "0.84.5", + "metro": "0.84.4", + "metro-cache": "0.84.4", + "metro-core": "0.84.4", + "metro-runtime": "0.84.4", "yaml": "^2.6.1" }, "engines": { @@ -34552,23 +34636,23 @@ } }, "node_modules/metro-core": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.5.tgz", - "integrity": "sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.4.tgz", + "integrity": "sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.84.5" + "metro-resolver": "0.84.4" }, "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-file-map": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.5.tgz", - "integrity": "sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.4.tgz", + "integrity": "sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -34625,9 +34709,9 @@ } }, "node_modules/metro-minify-terser": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.5.tgz", - "integrity": "sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.4.tgz", + "integrity": "sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -34638,9 +34722,9 @@ } }, "node_modules/metro-resolver": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.5.tgz", - "integrity": "sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.4.tgz", + "integrity": "sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -34650,9 +34734,9 @@ } }, "node_modules/metro-runtime": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.5.tgz", - "integrity": "sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.4.tgz", + "integrity": "sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -34663,18 +34747,18 @@ } }, "node_modules/metro-source-map": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.5.tgz", - "integrity": "sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.4.tgz", + "integrity": "sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.84.5", + "metro-symbolicate": "0.84.4", "nullthrows": "^1.1.1", - "ob1": "0.84.5", + "ob1": "0.84.4", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -34692,14 +34776,14 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.5.tgz", - "integrity": "sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.4.tgz", + "integrity": "sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.84.5", + "metro-source-map": "0.84.4", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" @@ -34721,9 +34805,9 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.5.tgz", - "integrity": "sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.4.tgz", + "integrity": "sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -34738,9 +34822,9 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.5.tgz", - "integrity": "sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.4.tgz", + "integrity": "sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -34748,13 +34832,13 @@ "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", - "metro": "0.84.5", - "metro-babel-transformer": "0.84.5", - "metro-cache": "0.84.5", - "metro-cache-key": "0.84.5", - "metro-minify-terser": "0.84.5", - "metro-source-map": "0.84.5", - "metro-transform-plugins": "0.84.5", + "metro": "0.84.4", + "metro-babel-transformer": "0.84.4", + "metro-cache": "0.84.4", + "metro-cache-key": "0.84.4", + "metro-minify-terser": "0.84.4", + "metro-source-map": "0.84.4", + "metro-transform-plugins": "0.84.4", "nullthrows": "^1.1.1" }, "engines": { @@ -34942,9 +35026,7 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "3.1.2", "devOptional": true, "license": "ISC", "dependencies": { @@ -35091,9 +35173,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -35352,9 +35434,9 @@ "license": "MIT" }, "node_modules/ob1": { - "version": "0.84.5", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.5.tgz", - "integrity": "sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.4.tgz", + "integrity": "sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -36205,22 +36287,6 @@ "node": ">=4" } }, - "node_modules/path-expression-matcher": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", - "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "devOptional": true, @@ -36339,9 +36405,7 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "2.3.1", "license": "MIT", "engines": { "node": ">=8.6" @@ -36612,9 +36676,9 @@ } }, "node_modules/postcss": { - "version": "8.5.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", - "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -36631,9 +36695,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.19", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", - "source-map-js": "^1.2.2" + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -36778,21 +36842,22 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.6.6", + "version": "7.5.3", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", + "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", - "long": "^5.3.2" + "long": "^5.0.0" }, "engines": { "node": ">=12.0.0" @@ -36917,6 +36982,13 @@ "dev": true, "license": "MIT" }, + "node_modules/queue": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "devOptional": true, @@ -37414,12 +37486,14 @@ } }, "node_modules/react-native-gesture-handler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-3.3.0.tgz", - "integrity": "sha512-ZCTlJ09nE3GPbm7J74lvwpxmx1TQ/7MDlHZNLARVYTN82k21mJUJ7BmI9eWjEi8eevHiAudI5O1Cpnt0rJfoJA==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz", + "integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==", "license": "MIT", "dependencies": { + "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", + "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { @@ -37626,9 +37700,9 @@ } }, "node_modules/react-native-nitro-sqlite": { - "version": "9.8.3", - "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.8.3.tgz", - "integrity": "sha512-pw4ZNYNQiMiyJZr6bu0dg+IPTskoEngg1wZBD/H5bg1oyH1rJM4HfS5nVgU7jydibIMko1YhrHK+3m5AEndcDg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.6.0.tgz", + "integrity": "sha512-a/N1yGhM8RvCCnaYhEHhh35YS+HDOAcGKeKFsp2ExCzIjP8vPXuzQtHylgLQLeAh7rUaism5q0QQFfogXm1SXA==", "license": "MIT", "dependencies": { "typeorm": "0.3.27" @@ -37805,9 +37879,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.116", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.116.tgz", - "integrity": "sha512-390oUHqYrPxEjdBtqYoay0FfIrT9zUqq2YsyqwNOC1xIXn6/+aIGycTJ0kIJEpkbIN7MN8IdMs3smwDMfiSvGg==", + "version": "3.0.115", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#b3c781503af7ba879149c028cc0c36523e8a7259", + "integrity": "sha512-r+4E5BTRQe1FcaJZrlcx09gle+seMBZDeVCbysnKLkhRGIrCt/GmuKNO92dLctFypvtKY5ecyXnhLEsv3KaIwg==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -38115,9 +38189,9 @@ "license": "MIT" }, "node_modules/react-native-safe-area-context": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.9.1.tgz", - "integrity": "sha512-Zpx9Iwg6VhgNarWFpcIM61xK2lUbSfSXemQUmcvOVRpux49lJsUompY1S3E5jKUJbLq233qEpj28DgmXVsgZMQ==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", + "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "license": "MIT", "peerDependencies": { "react": "*", @@ -38125,9 +38199,9 @@ } }, "node_modules/react-native-screens": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.28.0.tgz", - "integrity": "sha512-0/79Z8RCibuaAHti+DJ2Dq0m6UFWh4+p1bH58K3cjiQ7IDWs5kU9tCAj4lXcJMHHonbF2mIE2BeaPSEvOpIiFg==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.25.0.tgz", + "integrity": "sha512-CoE6W0perui0W4WK9fZFJfikUql/AYQFSJjnOGoXcPeteFb5Tursfmkot3vPOSu9lKWQMO6tlCIBQTC1CgbVRw==", "license": "MIT", "dependencies": { "react-freeze": "^1.0.0", @@ -38135,7 +38209,7 @@ }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": ">=0.82.0" } }, "node_modules/react-native-share": { @@ -39695,9 +39769,9 @@ } }, "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -39797,16 +39871,14 @@ "license": "ISC" }, "node_modules/simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", "dev": true, "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", "debug": "^4.4.0" }, "funding": { @@ -40075,9 +40147,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -40226,6 +40298,18 @@ "node": ">=14" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stack-generator": { "version": "2.0.10", "license": "MIT", @@ -41329,20 +41413,9 @@ } }, "node_modules/strnum": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", - "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", + "version": "1.0.5", "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "anynum": "^1.0.1" - } + "license": "MIT" }, "node_modules/structured-headers": { "version": "0.4.1", @@ -42374,15 +42447,13 @@ } }, "node_modules/underscore": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "version": "1.13.6", "license": "MIT" }, "node_modules/undici": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", - "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz", + "integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -42946,7 +43017,7 @@ } }, "node_modules/websocket-driver": { - "version": "0.7.5", + "version": "0.7.4", "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", @@ -43121,6 +43192,24 @@ "dev": true, "license": "MIT" }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, @@ -43688,10 +43777,19 @@ } }, "node_modules/xlsx": { - "version": "0.20.3", - "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", - "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, "bin": { "xlsx": "bin/xlsx.njs" }, @@ -43699,22 +43797,6 @@ "node": ">=0.8" } }, - "node_modules/xml-naming": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", - "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/xml2js": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", diff --git a/package.json b/package.json index 6b353b310e9e..bcf05e3fc6d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.5.5-0", + "version": "9.4.92-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -97,7 +97,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.342", + "@expensify/react-native-live-markdown": "0.1.336", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", @@ -138,13 +138,12 @@ "array.prototype.tosorted": "^1.1.4", "awesome-phonenumber": "^5.4.0", "canvas-size": "^1.2.6", - "canvaskit-wasm": "0.41.0", "d3-scale": "^4.0.2", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.207", + "expensify-common": "2.0.201", "expo": "57.0.8", "expo-asset": "57.0.7", "expo-audio": "57.0.3", @@ -166,11 +165,11 @@ "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", "json5": "2.2.2", - "lodash-es": "4.18.1", + "lodash-es": "4.17.21", "lottie-react-native": "7.3.8", "mapbox-gl": "^3.24.0", - "metro": "0.84.5", - "metro-transform-plugins": "0.84.5", + "metro": "0.84.4", + "metro-transform-plugins": "0.84.4", "onfido-sdk-ui": "14.53.1", "pako": "^2.1.0", "process": "^0.11.10", @@ -192,7 +191,7 @@ "react-native-device-info": "10.3.1", "react-native-draggable-flatlist": "^4.0.3", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "3.3.0", + "react-native-gesture-handler": "2.32.0", "react-native-google-places-autocomplete": "2.6.4", "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.1.2", @@ -202,8 +201,8 @@ "react-native-localize": "^3.5.4", "react-native-nitro-fetch": "1.6.2", "react-native-nitro-modules": "0.37.1", - "react-native-nitro-sqlite": "9.8.3", - "react-native-onyx": "3.0.116", + "react-native-nitro-sqlite": "9.6.0", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b3c781503af7ba879149c028cc0c36523e8a7259", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -214,8 +213,8 @@ "react-native-quick-crypto": "^1.1.6", "react-native-reanimated": "4.5.5", "react-native-render-html": "6.3.1", - "react-native-safe-area-context": "5.9.1", - "react-native-screens": "4.28.0", + "react-native-safe-area-context": "5.6.2", + "react-native-screens": "4.25.0", "react-native-share": "11.0.2", "react-native-svg": "15.15.5", "react-native-tab-view": "^4.3.0", @@ -230,7 +229,7 @@ "react-webcam": "^7.1.1", "scheduler": "0.27.0", "victory-native": "^41.21.0", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" + "xlsx": "^0.18.5" }, "devDependencies": { "@aaroon/workbox-rspack-plugin": "^1.0.1", @@ -390,11 +389,10 @@ "json5": "2.2.2", "loader-utils": "2.0.4", "follow-redirects": "1.15.6", - "fast-xml-parser": "5.11.1", + "fast-xml-parser": "4.4.1", "express": "4.20.0", "elliptic": "6.5.7", "fast-json-patch": "3.1.1", - "image-size": "2.0.4", "hermes-compiler": "250829098.0.14", "webpack": "^5.108.4", "esbuild": "^0.28.1", @@ -403,7 +401,6 @@ "path-to-regexp": "0.1.10", "send": "0.19.0", "regexpu-core": "6.4.0", - "adm-zip": "0.6.1", "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "react": "19.2.3", "react-dom": "19.2.3", @@ -424,6 +421,9 @@ "@react-native-firebase/app": { "expo": "57.0.8" }, + "@react-native-firebase/perf": { + "expo": "57.0.8" + }, "@react-native-google-signin/google-signin": { "expo": "57.0.8" }, @@ -453,10 +453,6 @@ } } }, - "nitroSQLite": { - "threadSafe": true, - "performanceMode": true - }, "engines": { "bun": "1.3.14", "node": "26.5.0", diff --git a/scripts/checkOnyxConnectBypass.ts b/scripts/checkOnyxConnectBypass.ts index 7134732ade19..3acde839b457 100644 --- a/scripts/checkOnyxConnectBypass.ts +++ b/scripts/checkOnyxConnectBypass.ts @@ -2,7 +2,7 @@ import {file} from 'bun'; /** - * Fails the lint run when a new inline `eslint-disable` bypasses the Onyx.connect() ban. + * Fails the lint run when a new inline `eslint-disable` bypasses the Onyx.connect() ban or `rulesdir/no-unsafe-onyx-read`. * * The ban (`rulesdir/no-onyx-connect`, shipped by eslint-config-expensify) is a normal lint rule, * so an inline disable can silence it. The runner re-elevates those disables by scanning source @@ -19,22 +19,20 @@ import {file} from 'bun'; import {execFileSync} from 'node:child_process'; import path from 'node:path'; -import {collectDisableDirectivesFromSource, findNewBypasses} from './onyxConnectBypass'; +import type {BannedRule} from './onyxConnectBypass'; + +import {BANNED_RULES, collectDisableDirectivesFromSource, findNewBypasses} from './onyxConnectBypass'; const projectRoot = path.resolve(import.meta.dir, '..'); -/** Files among the lint targets that mention Onyx, connect, and eslint-disable. */ -function findCandidateFiles(targets: string[]): string[] { +/** Files among the lint targets that mention every one of `searchTerms`. */ +function findCandidateFiles(targets: string[], searchTerms: string[]): string[] { const pathSpecs = targets.length > 0 ? targets : ['.']; try { - const output = execFileSync( - 'git', - ['grep', '-lI', '--all-match', '--untracked', '--no-recurse-submodules', '-e', 'Onyx', '-e', 'connect', '-e', 'eslint-disable', '--', ...pathSpecs], - { - cwd: projectRoot, - encoding: 'utf8', - }, - ); + const output = execFileSync('git', ['grep', '-lI', '--all-match', '--untracked', '--no-recurse-submodules', ...searchTerms.flatMap((term) => ['-e', term]), '--', ...pathSpecs], { + cwd: projectRoot, + encoding: 'utf8', + }); return output.split('\n').filter(Boolean); } catch (error: unknown) { if (typeof error === 'object' && error !== null && 'status' in error && error.status === 1) { @@ -45,11 +43,13 @@ function findCandidateFiles(targets: string[]): string[] { } /** - * Checks `targets` for new Onyx.connect() ban bypasses, reporting any to stderr. - * Returns `true` if a new bypass was found (i.e. the caller should fail). + * Checks `targets` for new bypasses of `ban`, reporting any to stderr. + * Returns `true` if a new bypass was found. */ -async function checkOnyxConnectBypass(targets: string[]): Promise { - const candidates = findCandidateFiles(targets); +async function checkBan(targets: string[], ban: BannedRule): Promise { + const candidates = findCandidateFiles(targets, ban.searchTerms) + .map((relativePath) => relativePath.split(path.sep).join('/')) + .filter(ban.appliesTo); if (candidates.length === 0) { return false; } @@ -58,17 +58,17 @@ async function checkOnyxConnectBypass(targets: string[]): Promise { await Promise.all( candidates.map(async (relativePath) => { const source = await file(path.join(projectRoot, relativePath)).text(); - return collectDisableDirectivesFromSource(source, relativePath.split(path.sep).join('/')); + return collectDisableDirectivesFromSource(source, relativePath, ban); }), ) ).flat(); - const newBypasses = findNewBypasses(suppressed); + const newBypasses = findNewBypasses(suppressed, ban); if (newBypasses.length === 0) { return false; } - console.error('Onyx.connect() is banned and the ban cannot be bypassed with eslint-disable. Use the useOnyx() hook to read Onyx data instead.'); + console.error(ban.message); console.error('New bypasses found:'); for (const bypass of newBypasses) { console.error(` ${bypass.file}:${bypass.line}`); @@ -76,6 +76,11 @@ async function checkOnyxConnectBypass(targets: string[]): Promise { return true; } +async function checkOnyxConnectBypass(targets: string[]): Promise { + const results = await Promise.all(BANNED_RULES.map((ban) => checkBan(targets, ban))); + return results.some(Boolean); +} + if (import.meta.main) { checkOnyxConnectBypass(process.argv.slice(2)) .then((failed) => { diff --git a/scripts/onyxConnectBypass.ts b/scripts/onyxConnectBypass.ts index 993200680ad7..937f3e6b6771 100644 --- a/scripts/onyxConnectBypass.ts +++ b/scripts/onyxConnectBypass.ts @@ -1,5 +1,5 @@ /** - * Detection logic for new `eslint-disable` bypasses of the Onyx.connect() ban. + * Detection logic for new `eslint-disable` bypasses of the Onyx.connect() ban and `rulesdir/no-unsafe-onyx-read`. * * `rulesdir/no-onyx-connect` (shipped by eslint-config-expensify) is a normal lint rule, so an * inline `eslint-disable` can silence it. The lint runner re-elevates those disables by scanning @@ -7,7 +7,7 @@ * disable directive can reach this check because it does not go through ESLint's message pipeline. * * Blanket `eslint-disable` / `eslint-disable-next-line` with no rule list counts only when it - * covers a real Onyx.connect() call. Unrelated blanket comments (e.g. around ReportUtils) remain + * covers a real banned call: Onyx.connect(), or Onyx.get() for the read rule. Unrelated blanket comments (e.g. around ReportUtils) remain * ignored. Call sites are found via the Babel AST so comments and grouping parens cannot hide a * banned member access from a source scan. */ @@ -33,7 +33,43 @@ const GRANDFATHERED_BYPASSES = new Map([ ['src/libs/ReportNameUtils.ts', 2], ]); -/** A `no-onyx-connect` violation that an inline disable directive silenced. */ +type BannedRule = { + id: string; + name: string; + objects: Set; + methods: Set; + grandfathered: Map; + appliesTo: (file: string) => boolean; + searchTerms: string[]; + message: string; +}; + +const ONYX_CONNECT_BAN: BannedRule = { + id: BANNED_RULE_ID, + name: BANNED_RULE_NAME, + objects: new Set(['Onyx']), + methods: new Set(['connect']), + grandfathered: GRANDFATHERED_BYPASSES, + appliesTo: () => true, + searchTerms: ['Onyx', 'connect', 'eslint-disable'], + message: 'Onyx.connect() is banned and the ban cannot be bypassed with eslint-disable. Use the useOnyx() hook to read Onyx data instead.', +}; + +const ONYX_READ_BAN: BannedRule = { + id: 'rulesdir/no-unsafe-onyx-read', + name: 'no-unsafe-onyx-read', + objects: new Set(['Onyx']), + methods: new Set(['get']), + grandfathered: new Map([['src/setup/addUtilsToWindow.ts', 1]]), + appliesTo: (file) => file.startsWith('src/'), + searchTerms: ['Onyx', 'eslint-disable'], + message: + 'Onyx reads checked by no-unsafe-onyx-read cannot be silenced with eslint-disable. Fix the read instead: use useOnyx() for data a component renders or reacts to, and call Onyx.get() only from event handlers or useCallback bodies in components, pages and hooks.', +}; + +const BANNED_RULES: BannedRule[] = [ONYX_CONNECT_BAN, ONYX_READ_BAN]; + +/** A banned-rule violation that an inline disable directive silenced. */ type SuppressedBan = { file: string; line: number; @@ -115,7 +151,7 @@ function unwrapExpression(node: ASTNode): ASTNode { return current; } -function isOnyxConnectCall(node: ASTNode): boolean { +function isBannedCall(node: ASTNode, ban: BannedRule): boolean { // Optional chaining anywhere in the call (`Onyx?.connect(...)`, `Onyx.connect?.(...)`) produces // `OptionalCallExpression`/`OptionalMemberExpression` nodes instead of their non-optional // counterparts, so a blanket disable directive over one would otherwise silently bypass the ban. @@ -126,20 +162,20 @@ function isOnyxConnectCall(node: ASTNode): boolean { if ((callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') || callee.computed === true) { return false; } - if (!BabelASTUtils.isASTNode(callee.property) || callee.property.type !== 'Identifier' || callee.property.name !== 'connect') { + if (!BabelASTUtils.isASTNode(callee.property) || callee.property.type !== 'Identifier' || typeof callee.property.name !== 'string' || !ban.methods.has(callee.property.name)) { return false; } if (!BabelASTUtils.isASTNode(callee.object)) { return false; } const object = unwrapExpression(callee.object); - return object.type === 'Identifier' && object.name === 'Onyx'; + return object.type === 'Identifier' && typeof object.name === 'string' && ban.objects.has(object.name); } -function collectOnyxConnectCallOffsets(root: ASTNode): number[] { +function collectBannedCallOffsets(root: ASTNode, ban: BannedRule): number[] { const offsets: number[] = []; const visit = (node: ASTNode) => { - if (isOnyxConnectCall(node)) { + if (isBannedCall(node, ban)) { offsets.push(node.start); } for (const child of BabelASTUtils.children(node, NON_CHILD_KEYS)) { @@ -157,14 +193,14 @@ function normalizedDirectiveArgs(args: string): string { .trim(); } -function directiveTargetsBan(args: string): boolean { +function directiveTargetsBan(args: string, ban: BannedRule): boolean { const trimmed = normalizedDirectiveArgs(args); if (trimmed.length === 0) { return false; } return trimmed.split(',').some((part) => { const rule = part.trim(); - return rule === BANNED_RULE_ID || rule === BANNED_RULE_NAME || rule.endsWith(`/${BANNED_RULE_NAME}`); + return rule === ban.id || rule === ban.name || rule.endsWith(`/${ban.name}`); }); } @@ -191,7 +227,7 @@ function lineNumberAtOffset(source: string, offset: number): number { return line; } -function blanketDirectiveCoversCall(source: string, match: DirectiveMatch, callOffsets: number[], enableMatches: DirectiveMatch[]): boolean { +function blanketDirectiveCoversCall(source: string, match: DirectiveMatch, callOffsets: number[], enableMatches: DirectiveMatch[], ban: BannedRule): boolean { const directiveLine = lineNumberAtOffset(source, match.index); const kind = directiveKind(match); const directiveEnd = match.index + match.text.length; @@ -213,28 +249,28 @@ function blanketDirectiveCoversCall(source: string, match: DirectiveMatch, callO return false; } const enableArgs = directiveArgs(enableMatch); - return isBlanketDirective(enableArgs) || directiveTargetsBan(enableArgs); + return isBlanketDirective(enableArgs) || directiveTargetsBan(enableArgs, ban); }); return !reenabled; }); } /** - * Find disable directives in `source` that suppress `rulesdir/no-onyx-connect`. + * Find disable directives in `source` that suppress `ban`. * Line numbers are 1-based. Matches both full-line and trailing `eslint-disable-line`. */ -function collectDisableDirectivesFromSource(source: string, file: string): SuppressedBan[] { +function collectDisableDirectivesFromSource(source: string, file: string, ban: BannedRule = ONYX_CONNECT_BAN): SuppressedBan[] { const parsed = parseSource(source); if (!parsed) { return []; } const bans: SuppressedBan[] = []; - const callOffsets = collectOnyxConnectCallOffsets(parsed.root); + const callOffsets = collectBannedCallOffsets(parsed.root, ban); const enableMatches = collectDirectiveMatches(parsed.comments, source, 'enable'); for (const match of collectDirectiveMatches(parsed.comments, source, 'disable')) { const args = directiveArgs(match); - const targetsBan = directiveTargetsBan(args); - const coversBan = isBlanketDirective(args) && blanketDirectiveCoversCall(source, match, callOffsets, enableMatches); + const targetsBan = directiveTargetsBan(args, ban); + const coversBan = isBlanketDirective(args) && blanketDirectiveCoversCall(source, match, callOffsets, enableMatches, ban); if (!targetsBan && !coversBan) { continue; } @@ -246,7 +282,7 @@ function collectDisableDirectivesFromSource(source: string, file: string): Suppr } /** Return the suppressed bans that exceed the grandfathered allowance for their file. */ -function findNewBypasses(suppressedBans: readonly SuppressedBan[]): SuppressedBan[] { +function findNewBypasses(suppressedBans: readonly SuppressedBan[], rule: BannedRule = ONYX_CONNECT_BAN): SuppressedBan[] { const byFile = new Map(); for (const ban of suppressedBans) { const list = byFile.get(ban.file) ?? []; @@ -256,7 +292,7 @@ function findNewBypasses(suppressedBans: readonly SuppressedBan[]): SuppressedBa const newBypasses: SuppressedBan[] = []; for (const [file, bans] of byFile) { - const allowed = GRANDFATHERED_BYPASSES.get(file) ?? 0; + const allowed = rule.grandfathered.get(file) ?? 0; if (bans.length <= allowed) { continue; } @@ -266,5 +302,5 @@ function findNewBypasses(suppressedBans: readonly SuppressedBan[]): SuppressedBa return newBypasses; } -export {BANNED_RULE_ID, BANNED_RULE_NAME, GRANDFATHERED_BYPASSES, collectDisableDirectivesFromSource, findNewBypasses}; -export type {SuppressedBan}; +export {BANNED_RULE_ID, BANNED_RULE_NAME, BANNED_RULES, GRANDFATHERED_BYPASSES, ONYX_CONNECT_BAN, ONYX_READ_BAN, collectDisableDirectivesFromSource, findNewBypasses}; +export type {BannedRule, SuppressedBan}; diff --git a/src/setup/addUtilsToWindow.ts b/src/setup/addUtilsToWindow.ts index ec3935f4e2b3..e1147d5cd66b 100644 --- a/src/setup/addUtilsToWindow.ts +++ b/src/setup/addUtilsToWindow.ts @@ -24,24 +24,9 @@ export default function addUtilsToWindow() { } window.Onyx = Onyx as typeof Onyx & { - get: (key: CollectionKeyBase) => Promise; log: (key: CollectionKeyBase) => void; }; - // We intentionally do not offer an Onyx.get API because we believe it will lead to code patterns we don't want to use in this repo, but we can offer a workaround for the sake of debugging - window.Onyx.get = function (key: CollectionKeyBase) { - return new Promise((resolve) => { - // We have opted for `connectWithoutView` here as this is a debugging utility and does not relate to any view. - const connection = Onyx.connectWithoutView({ - key, - callback: (value) => { - Onyx.disconnect(connection); - resolve(value); - }, - }); - }); - }; - window.Onyx.log = function (key: CollectionKeyBase) { window.Onyx.get(key).then((value) => { /* eslint-disable-next-line no-console */ diff --git a/src/types/modules/react-native-onyx.d.ts b/src/types/modules/react-native-onyx.d.ts index 73615460afd9..c091f53bce96 100644 --- a/src/types/modules/react-native-onyx.d.ts +++ b/src/types/modules/react-native-onyx.d.ts @@ -16,7 +16,6 @@ declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { Onyx: typeof Onyx & { - get: (key: CollectionKeyBase) => Promise; log: (key: CollectionKeyBase) => void; }; } diff --git a/tests/unit/NoUnsafeOnyxReadRuleTest.ts b/tests/unit/NoUnsafeOnyxReadRuleTest.ts new file mode 100644 index 000000000000..662e2f16121e --- /dev/null +++ b/tests/unit/NoUnsafeOnyxReadRuleTest.ts @@ -0,0 +1,446 @@ +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +import type {Rule} from 'eslint'; + +import {Linter, RuleTester} from 'eslint'; +import path from 'path'; +import {parser as tsParser} from 'typescript-eslint'; + +type LocalRuleModule = Rule.RuleModule & { + name: string; +}; + +function isLocalRuleModule(ruleModule: unknown): ruleModule is LocalRuleModule { + if (typeof ruleModule !== 'object' || ruleModule === null) { + return false; + } + + const ruleName: unknown = Reflect.get(ruleModule, 'name'); + const create: unknown = Reflect.get(ruleModule, 'create'); + const meta: unknown = Reflect.get(ruleModule, 'meta'); + + return typeof ruleName === 'string' && typeof create === 'function' && typeof meta === 'object' && meta !== null; +} + +const ruleModule: unknown = require('../../eslint-plugin-local-rules/no-unsafe-onyx-read'); + +if (!isLocalRuleModule(ruleModule)) { + throw new TypeError('Expected no-unsafe-onyx-read to export an ESLint rule module.'); +} + +const localRule: LocalRuleModule = ruleModule; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: { + ecmaFeatures: {jsx: true}, + }, + }, +}); + +const tsRuleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parser: tsParser, + parserOptions: { + ecmaFeatures: {jsx: true}, + }, + }, +}); + +const ONYX_IMPORT = "import Onyx from 'react-native-onyx';"; +const ONYX_UTILS_IMPORT = "import OnyxUtils from 'react-native-onyx/dist/OnyxUtils';"; + +const RENDER_ERRORS = [{messageId: 'noOnyxGetInRender'}]; +const MODULE_SCOPE_ERRORS = [{messageId: 'noOnyxReadAtModuleScope'}]; +const EFFECT_ERRORS = [{messageId: 'noOnyxReadInEffect'}]; +const OUTSIDE_ALLOWED_PATH_ERRORS = [{messageId: 'noOnyxReadOutsideAllowedPath'}]; + +const REPO_ROOT = path.resolve(__dirname, '../..'); + +function inRepo(relativePath: string): string { + return path.join(REPO_ROOT, relativePath); +} + +describe('no-unsafe-onyx-read', () => { + ruleTester.run(ruleModule.name, ruleModule, { + valid: [ + `${ONYX_IMPORT} export function submit() { const draft = Onyx.get(ONYXKEYS.SESSION); return draft; }`, + `${ONYX_IMPORT} export default function handler() { return Onyx.get(ONYXKEYS.SESSION); }`, + `${ONYX_IMPORT} const handlers = {onPress: () => Onyx.get(ONYXKEYS.SESSION)};`, + `${ONYX_IMPORT} class Store { read() { return Onyx.get(ONYXKEYS.SESSION); } }`, + + `${ONYX_IMPORT} function Row() { const onPress = () => Onyx.get(ONYXKEYS.SESSION); return ; }`, + `${ONYX_IMPORT} function Row() { return Onyx.get(ONYXKEYS.SESSION)} />; }`, + `${ONYX_IMPORT} function Row() { function onPress() { return Onyx.get(ONYXKEYS.SESSION); } return ; }`, + `${ONYX_IMPORT} function Row() { const onPress = async () => { await save(); return Onyx.get(ONYXKEYS.SESSION); }; return ; }`, + + `${ONYX_IMPORT} function Row() { const onPress = useCallback(() => Onyx.get(ONYXKEYS.SESSION), []); return ; }`, + `${ONYX_IMPORT} function Row() { useOnyx(key, {onLoaded: () => Onyx.get(ONYXKEYS.ACCOUNT)}); return ; }`, + `${ONYX_IMPORT} client.configure({selector: () => Onyx.get(ONYXKEYS.SESSION)});`, + `${ONYX_IMPORT} function setup() { client.configure({selector: () => Onyx.get(ONYXKEYS.SESSION)}); }`, + `${ONYX_IMPORT} const selector = (data) => Onyx.get(ONYXKEYS.SESSION); client.configure({selector});`, + `${ONYX_IMPORT} function Row() { const [v] = useReducer((state, action) => Onyx.get(ONYXKEYS.SESSION), 0); return ; }`, + `${ONYX_IMPORT} function Row() { const v = useSyncExternalStore((notify) => { Onyx.get(ONYXKEYS.SESSION); return noop; }, snapshot); return ; }`, + `${ONYX_IMPORT} function Row() { const onPress = () => Onyx.get(ONYXKEYS.SESSION).then(setValue); return ; }`, + `${ONYX_IMPORT} class Row extends React.Component { componentDidMount() { Onyx.get(ONYXKEYS.SESSION).then(this.setValue); } }`, + + `${ONYX_IMPORT} setTimeout(() => Onyx.get(ONYXKEYS.SESSION), 0);`, + `${ONYX_IMPORT} ready.then(() => Onyx.get(ONYXKEYS.SESSION));`, + `${ONYX_IMPORT} new Promise((resolve) => { ready.then(() => resolve(Onyx.get(ONYXKEYS.SESSION))); });`, + `${ONYX_IMPORT} Onyx.init(config).then(() => Onyx.get(ONYXKEYS.SESSION));`, + + `${ONYX_UTILS_IMPORT} async function f(key) { const {details} = await somethingElse(key); details.name = 'x'; return details; }`, + + `${ONYX_IMPORT} const api = window.somethingElse; function Row() { const value = api.get(ONYXKEYS.SESSION); return ; }`, + `${ONYX_IMPORT} function f(key) { const pending = Onyx.get(ONYXKEYS.SESSION); pending.name = 'x'; return pending; }`, + `${ONYX_IMPORT} function f(key) { const pending = Onyx.get(ONYXKEYS.SESSION); pending.push(1); return pending; }`, + + 'const Onyx = {get: () => undefined}; const initialValue = Onyx.get(ONYXKEYS.SESSION);', + 'const Onyx = {get: () => undefined}; function Row() { const value = Onyx.get(ONYXKEYS.SESSION); return ; }', + 'const initialValue = window.Onyx.get(ONYXKEYS.SESSION);', + 'function Row() { return ; }', + + `${ONYX_IMPORT} Onyx.init({keys: ONYXKEYS});`, + `${ONYX_IMPORT} function Row() { Onyx.merge(key, value); return ; }`, + `${ONYX_IMPORT} function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, + + `${ONYX_IMPORT} function submit(policyID) { Onyx.mergeCollection(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, values); return Onyx.get(\`\${ONYXKEYS.COLLECTION.POLICY_TAGS}\${policyID}\`); }`, + + `${ONYX_IMPORT} function submit() { if (shouldWrite) { Onyx.merge(key, value); } else { use(Onyx.get(ONYXKEYS.SESSION)); } }`, + `${ONYX_IMPORT} function submit(action) { switch (action) { case 'write': Onyx.merge(key, value); break; case 'read': use(Onyx.get(ONYXKEYS.SESSION)); break; } }`, + + 'const Onyx = {merge: () => {}, get: () => undefined}; function submit() { Onyx.merge(key, value); return Onyx.get(ONYXKEYS.SESSION); }', + ], + invalid: [ + {code: `${ONYX_IMPORT} new Promise((resolve) => { resolve(Onyx.get(ONYXKEYS.SESSION)); });`, errors: MODULE_SCOPE_ERRORS}, + + {code: `${ONYX_IMPORT} function Row() { const value = use(Onyx.get(ONYXKEYS.SESSION)); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { Onyx.get(ONYXKEYS.SESSION).then(setValue); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function useReportName() { return use(Onyx.get(ONYXKEYS.SESSION)); }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { const value = React.use(Onyx.get(ONYXKEYS.SESSION)); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { const promise = Onyx.get(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} async function Row() { const value = await Onyx.get(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} const Row = forwardRef((props, ref) => { Onyx.get(ONYXKEYS.SESSION).then(setValue); return ; });`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} class Row extends React.Component { render() { Onyx.get(ONYXKEYS.SESSION).then(this.setValue); return ; } }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function useReportName() { Onyx.get(ONYXKEYS.SESSION).then(setState); }`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} function Row() { const value = Onyx.get(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} const Row = () => { const value = Onyx.get(ONYXKEYS.SESSION); return ; };`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function useThing() { return Onyx.get(ONYXKEYS.SESSION); }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} const useReportName = () => Onyx.get(ONYXKEYS.SESSION);`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} export default function() { const value = Onyx.get(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} const Row = memo(() => { const value = Onyx.get(ONYXKEYS.SESSION); return ; });`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} const Row = memo(function () { const value = Onyx.get(ONYXKEYS.SESSION); return ; });`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} const Row = forwardRef((props, ref) => { const value = Onyx.get(ONYXKEYS.SESSION); return ; });`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} function Row() { return {Onyx.get(ONYXKEYS.SESSION)}; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { return ; }`, errors: RENDER_ERRORS}, + + { + code: `${ONYX_IMPORT} function Row() { const value = useSyncExternalStore(subscribe, () => Onyx.get(ONYXKEYS.SESSION)); return ; }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { const value = useSyncExternalStore(subscribe, snapshot, () => Onyx.get(ONYXKEYS.SESSION)); return ; }`, + errors: RENDER_ERRORS, + }, + + {code: `${ONYX_IMPORT} function Row() { const value = useMemo(() => Onyx.get(ONYXKEYS.SESSION), []); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { const value = React.useMemo(() => Onyx.get(ONYXKEYS.SESSION), []); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { const [value] = useState(() => Onyx.get(ONYXKEYS.SESSION)); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { const [value] = useReducer(reducer, key, (k) => Onyx.get(ONYXKEYS.SESSION)); return ; }`, errors: RENDER_ERRORS}, + + { + code: `${ONYX_IMPORT} function Row() { const [value] = useOnyx(key, {selector: (data) => Onyx.get(ONYXKEYS.ACCOUNT)}); return ; }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} function useThing() { return useOnyx(key, {selector: (data) => { const extra = Onyx.get(ONYXKEYS.ACCOUNT); return {...data, extra}; }}); }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { const p = usePolicy(id, {selector: (data) => Onyx.get(ONYXKEYS.ACCOUNT)}); return ; }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { const selector = (data) => Onyx.get(ONYXKEYS.ACCOUNT); const [value] = useOnyx(key, {selector}); return ; }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { const pickAccount = (data) => Onyx.get(ONYXKEYS.ACCOUNT); const [value] = useOnyx(key, {selector: pickAccount}); return ; }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} function pickAccount(data) { return Onyx.get(ONYXKEYS.ACCOUNT); } function Row() { const [value] = useOnyx(key, {selector: pickAccount}); return ; }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} const pickAccount = (data) => Onyx.get(ONYXKEYS.ACCOUNT); const selector = pickAccount; function Row() { const [value] = useOnyx(key, {selector}); return ; }`, + errors: RENDER_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { const compute = () => Onyx.get(ONYXKEYS.SESSION); const value = useMemo(compute, []); return ; }`, + errors: RENDER_ERRORS, + }, + + {code: `${ONYX_IMPORT} function Row() { const value = (() => Onyx.get(ONYXKEYS.SESSION))(); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { new Promise(() => Onyx.get(ONYXKEYS.SESSION)); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { new Promise((resolve) => { resolve(Onyx.get(ONYXKEYS.SESSION)); }); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { const values = ids.map((id) => Onyx.get(ONYXKEYS.SESSION)); return ; }`, errors: RENDER_ERRORS}, + { + code: `${ONYX_IMPORT} function Row() { const values = ids.filter((id) => Onyx.get(ONYXKEYS.SESSION)).map((id) => id); return ; }`, + errors: RENDER_ERRORS, + }, + + {code: `${ONYX_IMPORT} function Row() { const value = Onyx['get'](ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} const {get} = Onyx; function Row() { const value = get(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + { + code: `${ONYX_IMPORT} const {get: readOnyx} = Onyx; function Row() { const value = readOnyx(ONYXKEYS.SESSION); return ; }`, + errors: RENDER_ERRORS, + }, + {code: `${ONYX_IMPORT} const readOnyx = Onyx.get; function Row() { const value = readOnyx(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + + { + code: `${ONYX_IMPORT} function Row() { const a = Onyx.get(ONYXKEYS.SESSION); const b = Onyx.get(ONYXKEYS.ACCOUNT); return ; }`, + errors: [{messageId: 'noOnyxGetInRender'}, {messageId: 'noOnyxGetInRender'}], + }, + + {code: `${ONYX_IMPORT} function row() { const el = ; return el; }`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} const initialValue = Onyx.get(ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} export const session = Onyx.get(ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} let cached; cached = Onyx.get(ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} if (shouldPreload) { const value = Onyx.get(ONYXKEYS.SESSION); use(value); }`, errors: MODULE_SCOPE_ERRORS}, + + {code: `${ONYX_IMPORT} const initialValue = (() => Onyx.get(ONYXKEYS.SESSION))();`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} const values = keys.map((key) => Onyx.get(ONYXKEYS.SESSION));`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} const present = keys.filter((key) => Onyx.get(ONYXKEYS.SESSION)).map((key) => key);`, errors: MODULE_SCOPE_ERRORS}, + + {code: `${ONYX_IMPORT} const initialValue = Onyx['get'](ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + + {code: `${ONYX_IMPORT} const {get} = Onyx; const initialValue = get(ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} const {get: readOnyx} = Onyx; const initialValue = readOnyx(ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} const readOnyx = Onyx.get; const initialValue = readOnyx(ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + + { + code: `${ONYX_IMPORT} const a = Onyx.get(ONYXKEYS.SESSION); const b = Onyx.get(ONYXKEYS.ACCOUNT);`, + errors: [{messageId: 'noOnyxReadAtModuleScope'}, {messageId: 'noOnyxReadAtModuleScope'}], + }, + + {code: `${ONYX_IMPORT} const el = ;`, errors: MODULE_SCOPE_ERRORS}, + + {code: `${ONYX_IMPORT} Onyx.merge(key, value); const restored = Onyx.get(ONYXKEYS.SESSION);`, errors: MODULE_SCOPE_ERRORS}, + + {code: `${ONYX_IMPORT} const api = Onyx; function Row() { const value = api.get(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + {code: `${ONYX_IMPORT} const api = Onyx; const alias = api; function Row() { return ; }`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} function Row() { Onyx.merge(key, value); const a = Onyx.get(ONYXKEYS.SESSION); return ; }`, errors: RENDER_ERRORS}, + ], + }); +}); + +const RESTRICTED_ERRORS = [{messageId: 'noRestrictedOnyxKey'}]; + +const UNRESOLVABLE_ERRORS = [{messageId: 'noUnresolvableOnyxKey'}]; + +describe('no-unsafe-onyx-read restricted keys', () => { + ruleTester.run(ruleModule.name, ruleModule, { + valid: [ + {code: `${ONYX_IMPORT} export function submit() { return Onyx.get(ONYXKEYS.SESSION); }`}, + + {code: `${ONYX_IMPORT} export function submit(id) { return Onyx.get(\`\${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}\${id}\`); }`}, + {code: `${ONYX_IMPORT} export function submit() { const key = ONYXKEYS.SESSION; return Onyx.get(ONYXKEYS.SESSION); }`}, + ], + invalid: [ + {code: `${ONYX_IMPORT} export function submit() { return Onyx.get(ONYXKEYS.COLLECTION.REPORT); }`, errors: RESTRICTED_ERRORS}, + { + code: `${ONYX_IMPORT} export function submit(reportID) { return Onyx.get(\`\${ONYXKEYS.COLLECTION.REPORT}\${reportID}\`); }`, + errors: RESTRICTED_ERRORS, + }, + + { + code: `${ONYX_IMPORT} export function submit() { const key = ONYXKEYS.COLLECTION.REPORT; return Onyx.get(key); }`, + errors: RESTRICTED_ERRORS, + }, + { + code: `${ONYX_IMPORT} export function submit(reportID) { const key = \`\${ONYXKEYS.COLLECTION.REPORT}\${reportID}\`; return Onyx.get(key); }`, + errors: RESTRICTED_ERRORS, + }, + {code: `${ONYX_IMPORT} export function submit(key) { return Onyx.get(key); }`, errors: UNRESOLVABLE_ERRORS}, + {code: `${ONYX_IMPORT} export function submit() { let key = ONYXKEYS.SESSION; key = other; return Onyx.get(key); }`, errors: UNRESOLVABLE_ERRORS}, + {code: `${ONYX_IMPORT} export function submit(id) { return Onyx.get(getTravelCardKey(id)); }`, errors: UNRESOLVABLE_ERRORS}, + { + code: `${ONYX_IMPORT} export function submit(formID) { return Onyx.get(\`\${formID}Draft\`); }`, + errors: UNRESOLVABLE_ERRORS, + }, + ], + }); +}); + +describe('no-unsafe-onyx-read under the TypeScript parser', () => { + tsRuleTester.run(ruleModule.name, ruleModule, { + valid: [ + {code: `${ONYX_IMPORT} export function submit(id: string) { return Onyx.get(\`\${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}\${id}\` as const); }`}, + {code: `${ONYX_IMPORT} export function submit(id: string) { const key = \`\${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}\${id}\` as const; return Onyx.get(key); }`}, + { + code: `${ONYX_IMPORT} export function submit(id: string) { const key = \`\${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}\${id}\` as typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES; return Onyx.get(key); }`, + }, + {code: `${ONYX_IMPORT} export function submit(id: string) { return Onyx.get(\`\${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}\${id}\` satisfies string); }`}, + {code: `${ONYX_IMPORT} export function submit() { return Onyx.get(ONYXKEYS.SESSION as OnyxKey); }`}, + {code: `${ONYX_IMPORT} export function submit() { return Onyx.get(ONYXKEYS.SESSION!); }`}, + {code: `${ONYX_IMPORT} export function submit(): Promise { return Onyx.get(ONYXKEYS.SESSION); }`}, + ], + invalid: [ + {code: `${ONYX_IMPORT} export function submit(reportID: string) { return Onyx.get(\`\${ONYXKEYS.COLLECTION.REPORT}\${reportID}\` as const); }`, errors: RESTRICTED_ERRORS}, + { + code: `${ONYX_IMPORT} export function submit(reportID: string) { const key = \`\${ONYXKEYS.COLLECTION.REPORT}\${reportID}\` as typeof ONYXKEYS.COLLECTION.REPORT; return Onyx.get(key); }`, + errors: RESTRICTED_ERRORS, + }, + {code: `${ONYX_IMPORT} export function submit() { return Onyx.get(ONYXKEYS.PERSONAL_DETAILS_LIST as OnyxKey); }`, errors: RESTRICTED_ERRORS}, + {code: `${ONYX_IMPORT} export function submit(key: OnyxKey) { return Onyx.get(key as OnyxKey); }`, errors: UNRESOLVABLE_ERRORS}, + + { + code: `${ONYX_IMPORT} function Row({id}: {id: string}) { const value = Onyx.get(ONYXKEYS.SESSION as OnyxKey); return ; }`, + errors: RENDER_ERRORS, + }, + {code: `${ONYX_IMPORT} const initialValue = Onyx.get(ONYXKEYS.SESSION as OnyxKey);`, errors: MODULE_SCOPE_ERRORS}, + ], + }); +}); + +describe('no-unsafe-onyx-read restricted keys', () => { + const linter = new Linter(); + + function isSearchSnapshotKey(value: string): boolean { + return !value.startsWith(ONYXKEYS.COLLECTION.SNAPSHOT) && CONST.SEARCH.SNAPSHOT_ONYX_KEYS.some((prefix) => value.startsWith(prefix)); + } + + function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; + } + + function collectKeyPaths(node: Record, prefix: string[] = []): Array<[string, string]> { + return Object.entries(node).flatMap<[string, string]>(([name, value]) => { + if (typeof value === 'string') { + return [[[...prefix, name].join('.'), value]]; + } + + return isRecord(value) ? collectKeyPaths(value, [...prefix, name]) : []; + }); + } + + function isRejected(keyPath: string): boolean { + const code = `${ONYX_IMPORT} export function submit() { return Onyx.get(ONYXKEYS.${keyPath}); }`; + const messages = linter.verify(code, { + plugins: {localRules: {rules: {[localRule.name]: localRule}}}, + languageOptions: {ecmaVersion: 2022, sourceType: 'module'}, + rules: {[`localRules/${localRule.name}`]: 'error'}, + }); + + return messages.some((message) => message.messageId === 'noRestrictedOnyxKey'); + } + + it('rejects exactly the ONYXKEYS entries a Search scope would redirect', () => { + const keyPaths = collectKeyPaths(ONYXKEYS); + expect(keyPaths.length).toBeGreaterThan(500); + + const disagreements = keyPaths.filter(([keyPath, value]) => isRejected(keyPath) !== isSearchSnapshotKey(value)); + + expect(disagreements).toEqual([]); + }); + + it('covers every Search snapshot prefix', () => { + const rejectedValues = collectKeyPaths(ONYXKEYS) + .filter(([keyPath]) => isRejected(keyPath)) + .map(([, value]) => value); + + for (const prefix of CONST.SEARCH.SNAPSHOT_ONYX_KEYS) { + expect(rejectedValues.some((value) => value.startsWith(prefix))).toBe(true); + } + }); +}); + +describe('no-unsafe-onyx-read effects', () => { + ruleTester.run(ruleModule.name, ruleModule, { + valid: [ + `${ONYX_IMPORT} function Row() { useEffect(() => { const subscription = emitter.addListener('change', () => Onyx.get(ONYXKEYS.SESSION)); return () => subscription.remove(); }, []); return ; }`, + `${ONYX_IMPORT} function Row() { const onChange = () => Onyx.get(ONYXKEYS.SESSION); useEffect(() => { window.addEventListener('focus', onChange); return () => window.removeEventListener('focus', onChange); }, [onChange]); return ; }`, + `${ONYX_IMPORT} function useThing() { const onShortcut = () => Onyx.get(ONYXKEYS.SESSION); useEffect(() => registerShortcut(onShortcut), [onShortcut]); }`, + `${ONYX_IMPORT} function Row() { const load = () => Onyx.get(ONYXKEYS.SESSION); useEffect(() => {}, [load]); return ; }`, + `${ONYX_IMPORT} function Row() { const load = useCallback(() => Onyx.get(ONYXKEYS.SESSION), []); const run = () => load(); return ; }`, + `${ONYX_IMPORT} function Row() { const onPress = () => Onyx.get(ONYXKEYS.SESSION); useEffect(() => { track(); }, []); return ; }`, + ], + invalid: [ + { + code: `${ONYX_IMPORT} function Row() { const load = () => Onyx.get(ONYXKEYS.SESSION); useEffect(() => { const id = setTimeout(load, 0); return () => clearTimeout(id); }, []); return ; }`, + errors: EFFECT_ERRORS, + }, + {code: `${ONYX_IMPORT} function Row() { useEffect(() => { items.forEach(() => Onyx.get(ONYXKEYS.SESSION)); }, []); return ; }`, errors: EFFECT_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { const onPress = () => Onyx.get(ONYXKEYS.SESSION); useEffect(onPress, []); return ; }`, errors: EFFECT_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { useEffect(() => { use(Onyx.get(ONYXKEYS.SESSION)); }, []); return ; }`, errors: EFFECT_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { useLayoutEffect(() => { use(Onyx.get(ONYXKEYS.SESSION)); }, []); return ; }`, errors: EFFECT_ERRORS}, + {code: `${ONYX_IMPORT} function Row() { useFocusEffect(useCallback(() => { Onyx.get(ONYXKEYS.SESSION); }, [])); return ; }`, errors: EFFECT_ERRORS}, + { + code: `${ONYX_IMPORT} function Row() { const onFocus = useCallback(() => { Onyx.get(ONYXKEYS.SESSION); }, []); useFocusEffect(onFocus); return ; }`, + errors: EFFECT_ERRORS, + }, + {code: `${ONYX_IMPORT} function Row() { useEffect(() => { ready.then(() => Onyx.get(ONYXKEYS.SESSION)); }, []); return ; }`, errors: EFFECT_ERRORS}, + { + code: `${ONYX_IMPORT} function Row() { const load = async () => { await Onyx.get(ONYXKEYS.SESSION); }; useEffect(() => { load(); }, []); return ; }`, + errors: EFFECT_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { function load() { return Onyx.get(ONYXKEYS.SESSION); } useLayoutEffect(() => { load(); }, []); return ; }`, + errors: EFFECT_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { const load = useCallback(async () => { await Onyx.get(ONYXKEYS.SESSION); }, []); useEffect(() => { load(); }, [load]); return ; }`, + errors: EFFECT_ERRORS, + }, + { + code: `${ONYX_IMPORT} function Row() { const load = () => Onyx.get(ONYXKEYS.SESSION); const run = () => { load(); }; useEffect(() => { run(); }, []); return ; }`, + errors: EFFECT_ERRORS, + }, + {code: `${ONYX_IMPORT} function useThing() { const load = () => Onyx.get(ONYXKEYS.SESSION); useEffect(() => { ready.then(load); }, []); }`, errors: EFFECT_ERRORS}, + ], + }); +}); + +describe('no-unsafe-onyx-read allowed paths', () => { + ruleTester.run(ruleModule.name, ruleModule, { + valid: [ + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: inRepo('src/components/Foo.tsx')}, + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: inRepo('src/pages/Foo.tsx')}, + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: inRepo('src/hooks/useFoo.ts')}, + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: inRepo('tests/unit/FooTest.ts')}, + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: 'file.ts'}, + ], + invalid: [ + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: inRepo('src/libs/actions/Foo.ts'), errors: OUTSIDE_ALLOWED_PATH_ERRORS}, + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: inRepo('src/libs/ReportUtils.ts'), errors: OUTSIDE_ALLOWED_PATH_ERRORS}, + { + code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, + filename: inRepo('src/setup/addUtilsToWindow.ts'), + errors: OUTSIDE_ALLOWED_PATH_ERRORS, + }, + {code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.SESSION); }`, filename: inRepo('src/CONST/index.ts'), errors: OUTSIDE_ALLOWED_PATH_ERRORS}, + { + code: `${ONYX_IMPORT} const {get} = Onyx; export async function submit() { return get(ONYXKEYS.SESSION); }`, + filename: inRepo('src/libs/Foo.ts'), + errors: OUTSIDE_ALLOWED_PATH_ERRORS, + }, + { + code: `${ONYX_IMPORT} export async function submit() { return Onyx.get(ONYXKEYS.COLLECTION.REPORT); }`, + filename: inRepo('src/libs/Foo.ts'), + errors: OUTSIDE_ALLOWED_PATH_ERRORS, + }, + ], + }); +}); diff --git a/tests/unit/OnyxConnectBypassTest.ts b/tests/unit/OnyxConnectBypassTest.ts index 50afee0cbd20..fa1e2baeb8a9 100644 --- a/tests/unit/OnyxConnectBypassTest.ts +++ b/tests/unit/OnyxConnectBypassTest.ts @@ -1,4 +1,4 @@ -import {BANNED_RULE_ID, collectDisableDirectivesFromSource, findNewBypasses} from '../../scripts/onyxConnectBypass'; +import {BANNED_RULE_ID, ONYX_READ_BAN, collectDisableDirectivesFromSource, findNewBypasses} from '../../scripts/onyxConnectBypass'; const ONYX_CONNECT_CALL = `Onyx${'.connect'}`; const onyxConnectCall = (key: string): string => `${ONYX_CONNECT_CALL}({key: "${key}"});`; @@ -141,3 +141,69 @@ describe('findNewBypasses', () => { expect(findNewBypasses([])).toEqual([]); }); }); + +describe('no-unsafe-onyx-read bypasses', () => { + const onyxReadCall = `await Onyx${'.get'}(ONYXKEYS.SESSION);`; + + it('flags a disable that names the rule', () => { + // Given a src file that silences no-unsafe-onyx-read on the next line + const source = ['// eslint-disable-next-line rulesdir/no-unsafe-onyx-read', onyxReadCall].join('\n'); + + // When the read ban scans it + const suppressed = collectDisableDirectivesFromSource(source, 'src/libs/Foo.ts', ONYX_READ_BAN); + + // Then the directive counts as a bypass, since the rule must not be silenced inline + expect(suppressed).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); + }); + + it('flags a blanket disable that covers an Onyx.get call', () => { + // Given a blanket disable over a read + const source = ['/* eslint-disable */', onyxReadCall].join('\n'); + + // When the read ban scans it + // Then it counts, because a blanket directive silences the rule just as well as a named one + expect(collectDisableDirectivesFromSource(source, 'src/libs/Foo.ts', ONYX_READ_BAN)).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); + }); + + it('ignores a blanket disable that covers no read', () => { + // Given a blanket disable over code that never reads Onyx + const source = ['/* eslint-disable */', 'console.log(1);'].join('\n'); + + // When the read ban scans it + // Then nothing is reported, so unrelated blanket comments keep working + expect(collectDisableDirectivesFromSource(source, 'src/libs/Foo.ts', ONYX_READ_BAN)).toEqual([]); + }); + + it('does not treat a no-onyx-connect disable as a read bypass', () => { + // Given a disable of the connect ban + const source = [`// eslint-disable-next-line ${BANNED_RULE_ID}`, onyxReadCall].join('\n'); + + // When the read ban scans it + // Then it is left to the connect ban, since each ban only owns its own rule + expect(collectDisableDirectivesFromSource(source, 'src/libs/Foo.ts', ONYX_READ_BAN)).toEqual([]); + }); + + it('allows only the grandfathered addUtilsToWindow disable', () => { + // Given the existing addUtilsToWindow disable plus one more in that file and one elsewhere + const bans = [ + {file: 'src/setup/addUtilsToWindow.ts', line: 1}, + {file: 'src/setup/addUtilsToWindow.ts', line: 40}, + {file: 'src/libs/Foo.ts', line: 3}, + ]; + + // When the read ban compares them with its grandfather list + // Then only the file-level dev-console disable survives + expect(findNewBypasses(bans, ONYX_READ_BAN)).toEqual([ + {file: 'src/setup/addUtilsToWindow.ts', line: 40}, + {file: 'src/libs/Foo.ts', line: 3}, + ]); + }); + + it('covers src but not tests', () => { + // Given a src file and a test suite + // When the read ban decides which files it checks + // Then tests are out of scope, since reads are allowed there and suites assert on Search snapshot keys + expect(ONYX_READ_BAN.appliesTo('src/pages/Foo.tsx')).toBe(true); + expect(ONYX_READ_BAN.appliesTo('tests/unit/FooTest.ts')).toBe(false); + }); +}); From 495fa9cae25eef69bc4fa07c97c1f585250a92aa Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 24 Sep 2026 11:09:31 +0200 Subject: [PATCH 2/7] Cover Onyx.multiGet in no-unsafe-onyx-read, the bypass check and the ONYX-1 docs --- .../onyx-1-no-render-reachable-onyx-read.md | 16 +-- .../philosophies/ONYX-DATA-MANAGEMENT.md | 7 +- .../no-unsafe-onyx-read.js | 132 ++++++++++++++---- scripts/onyxConnectBypass.ts | 6 +- tests/unit/NoUnsafeOnyxReadRuleTest.ts | 42 ++++++ tests/unit/OnyxConnectBypassTest.ts | 9 ++ 6 files changed, 171 insertions(+), 41 deletions(-) diff --git a/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md b/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md index 196ad4f38274..485bad722e88 100644 --- a/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md +++ b/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md @@ -7,16 +7,16 @@ title: Keep Onyx reads off the render path and out of a written tick ### Reasoning -`await Onyx.get()` reads a key once and never subscribes. `no-unsafe-onyx-read` catches most misuse, so this rule covers only what lint can't see. +`await Onyx.get()` reads a key once and never subscribes. `Onyx.multiGet()` does the same for each key in an array, so everything below about `Onyx.get` applies to it too. `no-unsafe-onyx-read` catches most misuse, so this rule covers only what lint can't see. Do not re-check these: | Enforced | By | |---|---| -| `Onyx.get` outside `src/components`, `src/pages`, `src/hooks` and `tests` | `no-unsafe-onyx-read` | +| `Onyx.get` or `Onyx.multiGet` outside `src/components`, `src/pages`, `src/hooks` and `tests` | `no-unsafe-onyx-read` | | A read during render or at module scope | `no-unsafe-onyx-read` | | A read inside an effect, or in a same-file function an effect calls | `no-unsafe-onyx-read` | -| A Search snapshot key, or a key lint can't resolve | `no-unsafe-onyx-read` | +| A Search snapshot key, or a key lint can't resolve, including any `multiGet` element | `no-unsafe-onyx-read` | | A runtime import of `react-native-onyx/dist/OnyxUtils` | `@typescript-eslint/no-restricted-imports` | | An inline `eslint-disable` of the rule | `scripts/checkOnyxConnectBypass.ts` | | A missing `await` whose value is then used | `tsc` | @@ -142,8 +142,8 @@ return Current theme: {theme}; - A1. The diff adds a read to a function that some caller reaches from render: a component or hook body, a `useMemo` callback, a `useOnyx` selector, a lazy initializer, an IIFE or array callback in the body, or a local function the body calls. Grep `src/` for the function's name, ignoring imports. A plain-function caller isn't a verdict, so repeat on its name. Comment on the read, naming the calling file and line. - A2. The function holding the read returns JSX from any branch, or is passed as `renderItem`, `ListHeaderComponent`, or any `render*` or `*Component` prop. Flag the read. -- A3. The diff adds a call at a render position in a component or hook, the call's value is discarded or the callee returns `void`, and the callee's file contains `Onyx.get`. Comment on the call. -- A4. The diff passes a function that reads, or whose file contains `Onyx.get`, as a prop, and the receiver calls that prop from render. Open the receiver's file and Grep the prop's name followed by `(`; follow forwarded props. If the receiver can't be resolved (a spread, or a component held in a variable), ask the author to confirm nothing calls it during render. +- A3. The diff adds a call at a render position in a component or hook, the call's value is discarded or the callee returns `void`, and the callee's file contains `Onyx.get` or `Onyx.multiGet`. Comment on the call. +- A4. The diff passes a function that reads, or whose file contains `Onyx.get` or `Onyx.multiGet`, as a prop, and the receiver calls that prop from render. Open the receiver's file and Grep the prop's name followed by `(`; follow forwarded props. If the receiver can't be resolved (a spread, or a component held in a variable), ask the author to confirm nothing calls it during render. #### B. Tick @@ -152,7 +152,7 @@ return Current theme: {theme}; #### C. Effect in another file -- C1. The diff passes a function that reads, or whose file contains `Onyx.get`, to another component or hook, or turns a function already passed that way into one that reads. Open the receiver, Grep the prop's name followed by `(`, and flag when a call sits inside a `useEffect`, `useLayoutEffect` or `useFocusEffect` callback, directly or through a local function. Follow forwarded props. Comment on the prop, naming the receiver's effect. +- C1. The diff passes a function that reads, or whose file contains `Onyx.get` or `Onyx.multiGet`, to another component or hook, or turns a function already passed that way into one that reads. Open the receiver, Grep the prop's name followed by `(`, and flag when a call sits inside a `useEffect`, `useLayoutEffect` or `useFocusEffect` callback, directly or through a local function. Follow forwarded props. Comment on the prop, naming the receiver's effect. #### D. Output @@ -167,13 +167,13 @@ return Current theme: {theme}; - The removed `useOnyx` value appears nowhere in the diff except the converted call's arguments - The value is meant as a snapshot of the event, and nothing downstream expects it to update - The write is awaited, or the read runs in its `.then`, and the read key isn't derived from the written one -- The read sits in a deliberate deferral: a `.then`, a timer, `runAfterTransitions`, `runAfterInteractions`, or a callback passed to an async API. Don't suggest hoisting it above the deferral, since that pins the value to the moment before the wait +- (B only) The read sits in a deliberate deferral: a `.then`, a timer, `runAfterTransitions`, `runAfterInteractions`, or a callback passed to an async API. Don't suggest hoisting it above the deferral, since that pins the value to the moment before the wait. A `.then` chained on the read itself is not a deferral, and a deferral never excuses an A, C or D finding - The write and the read are in exclusive branches, or the write's branch returns first - The keys differ and the read key isn't derived from the written one **Search Patterns** (hints for reviewers): -- `Onyx.get(` +- `Onyx.get(`, `Onyx.multiGet(` - `Onyx.merge(`, `Onyx.update(`, `Onyx.set(`, `Onyx.mergeCollection(` - `ONYXKEYS.DERIVED` - removed `useOnyx(` lines in the diff, then that variable's name in the rest of the diff diff --git a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md index 1728459102d6..45ab5cc7ef0e 100644 --- a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md +++ b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md @@ -37,11 +37,12 @@ Different platforms come with varying storage capacities and Onyx has a way to g - Add the key to the `evictableKeys` option in `Onyx.init(options)` - A least recently accessed key will only be deleted when an Onyx operation retries after failing. -## Reading Onyx data: `useOnyx`, `Onyx.connectWithoutView` and `Onyx.get()` -There are three ways to read Onyx data, and `Onyx.connect` is deprecated: +## Reading Onyx data: `useOnyx`, `Onyx.connectWithoutView`, `Onyx.get()` and `Onyx.multiGet()` +There are four ways to read Onyx data, and `Onyx.connect` is deprecated: 1. **`useOnyx`** (from `@hooks/useOnyx`) — the default for anything a React component renders. 2. **`Onyx.connectWithoutView`** — an imperative subscription for non-render logic, used only when `useOnyx` genuinely does not fit. 3. **`Onyx.get()`**: an asynchronous, one-shot read of the cache that never subscribes, for event handlers in components, pages and hooks. +4. **`Onyx.multiGet()`**: a thin wrapper around `Onyx.get()` that calls it for each key in an array and resolves the values in the same order. Every `Onyx.get()` rule below applies to it and to each key it reads. ### - Prefer a pure function over reading Onyx at all A pure function does not read Onyx itself — it receives the data it needs as parameters, and its caller does the reading (with `useOnyx` or `Onyx.connectWithoutView`) and passes it in. Before adding either subscription, check whether the code can be a pure function instead: it needs no connection, is trivial to test, and cannot cause extra rerenders. Prefer this even when it means passing more arguments. This takes precedence over everything below. @@ -65,7 +66,7 @@ In rare cases a component that subscribes to multiple large collections through It reads the cache once and never subscribes, so the value it returns MUST NOT reach rendered output, directly or through state, a ref or a module variable. Use it in event handlers and `useCallback` bodies under `src/components`, `src/pages` and `src/hooks`. Never during render, at module scope, or in code an effect runs. ### - `Onyx.get()` MUST NOT read the Search snapshot keys -`@hooks/useOnyx` redirects the keys in `CONST.SEARCH.SNAPSHOT_ONYX_KEYS` to a Search snapshot inside a `SearchScopeProvider`, and `Onyx.get()` always reads the global key. These keys stay on `useOnyx`. +`@hooks/useOnyx` redirects the keys in `CONST.SEARCH.SNAPSHOT_ONYX_KEYS` to a Search snapshot inside a `SearchScopeProvider`, and `Onyx.get()` always reads the global key. These keys stay on `useOnyx`, and no element of an `Onyx.multiGet()` key list may be one of them. Write that list as an array literal of static keys, or a `const` bound to one, so lint can check each element. ### - Reads MUST come before a write in the same tick, or after the write is awaited `Onyx.get()` captures the cache when it is called, and most writes land later, so a read queued behind a write returns the old value. A derived key (`ONYXKEYS.DERIVED.*`) lags its sources, so read it only before writing them. diff --git a/eslint-plugin-local-rules/no-unsafe-onyx-read.js b/eslint-plugin-local-rules/no-unsafe-onyx-read.js index fb099ca86874..8a1da32892de 100644 --- a/eslint-plugin-local-rules/no-unsafe-onyx-read.js +++ b/eslint-plugin-local-rules/no-unsafe-onyx-read.js @@ -52,6 +52,10 @@ const RESTRICTED_KEY_PATHS = resolveRestrictedKeyPaths(); const READ_METHOD = 'get'; +const MULTI_READ_METHOD = 'multiGet'; + +const READ_METHODS = new Set([READ_METHOD, MULTI_READ_METHOD]); + const READ_ALLOWED_DIRECTORIES = ['src/components/', 'src/pages/', 'src/hooks/', 'tests/']; const READ_ALLOWED_FILES = new Set([]); @@ -90,25 +94,26 @@ const EFFECT = 'effect'; const meta = { type: 'problem', docs: { - description: 'Disallow unsafe Onyx reads: Onyx.get outside components, pages, hooks and tests, during render, inside effects, at module scope or on Search snapshot keys.', + description: + 'Disallow unsafe Onyx reads: Onyx.get or Onyx.multiGet outside components, pages, hooks and tests, during render, inside effects, at module scope or on Search snapshot keys.', recommended: 'error', }, schema: [], messages: { noOnyxGetInRender: - 'Do not read Onyx during render. Onyx.get() is a one-shot read that never subscribes, so a value obtained while rendering does not re-render the component when that key changes and the UI can show stale data indefinitely. A component cannot await it either, so reaching it from render means use() or .then(), both of which read without subscribing.\n\n' + - 'Use useOnyx() for anything the component renders. Reserve Onyx.get() for code that runs on an event: event handlers and useCallback bodies.', + 'Do not read Onyx during render. Onyx.get() and Onyx.multiGet() are one-shot reads that never subscribes, so a value they return while rendering does not re-render the component when that key changes and the UI can show stale data indefinitely. A component cannot await it either, so reaching it from render means use() or .then(), both of which read without subscribing.\n\n' + + 'Use useOnyx() for anything the component renders. Reserve Onyx.get() and Onyx.multiGet() for code that runs on an event: event handlers and useCallback bodies.', noOnyxReadAtModuleScope: 'Do not read Onyx at module scope. A module body runs at import time and cannot await, so the value can only be parked in a module variable through .then(), where it is a one-shot snapshot that never updates when the key changes.\n\n' + 'Move the read inside the function that needs it, so it runs at event time and reads the current value. If the module genuinely needs to track a key, subscribe with Onyx.connectWithoutView() instead of caching one read.', noUnresolvableOnyxKey: 'Do not read Onyx with a key this rule cannot resolve. The read surface is restricted to keys that are provably not Search snapshot keys, and a key built at runtime cannot be checked, so a caller can route a snapshot key here without anything failing.\n\n' + - 'Write the key as an ONYXKEYS access, such as ONYXKEYS.SESSION, or as a template literal that starts with an ONYXKEYS collection prefix. If the key cannot be static, keep the useOnyx subscription or take the value as a parameter. An inline eslint-disable of this rule fails the lint run.', + 'Write the key as an ONYXKEYS access, such as ONYXKEYS.SESSION, or as a template literal that starts with an ONYXKEYS collection prefix. For Onyx.multiGet(), pass an array literal, or a const bound to one, whose every element is written that way. If the key cannot be static, keep the useOnyx subscription or take the value as a parameter. An inline eslint-disable of this rule fails the lint run.', noRestrictedOnyxKey: 'Do not read {{keyPath}} with a one-shot Onyx read. src/hooks/useOnyx.ts rewrites this key to snapshot_ inside a SearchScopeProvider subtree, so a component subscribed to it may never have been reading the global key at all. A read here returns live data where the component saw the snapshot, and nothing at the call site can tell the two apart.\n\n' + 'Take the value as a parameter from the component, which knows whether it is inside a Search scope, or keep the useOnyx subscription.', noOnyxReadOutsideAllowedPath: - 'Onyx.get() is only allowed in src/components, src/pages, src/hooks and tests.\n\n' + + 'Onyx.get() and Onyx.multiGet() are only allowed in src/components, src/pages, src/hooks and tests.\n\n' + 'Elsewhere, take the value as a parameter or keep the Onyx.connectWithoutView() subscription. A file joins READ_ALLOWED_FILES only in a PR that removes an Onyx.connectWithoutView() from it.', noOnyxReadInEffect: 'Do not read Onyx inside an effect, or in a function an effect calls. When the value was in the effect dependency array, the useOnyx subscription is what re-runs the effect, and a one-shot read stops that.\n\n' + @@ -562,6 +567,27 @@ function classifyPosition(ancestors, sourceCode) { return MODULE_SCOPE; } +function getKeyListElements(node, scope, seen = new Set()) { + let current = node; + + while (current && TYPE_ONLY_EXPRESSIONS.has(current.type)) { + current = current.expression; + } + + if (current?.type === 'Identifier') { + if (seen.has(current)) { + return null; + } + + seen.add(current); + const initializer = getConstInitializer(current, scope); + + return initializer ? getKeyListElements(initializer, scope, seen) : null; + } + + return current?.type === 'ArrayExpression' ? current.elements : null; +} + function findRestrictedKey(keyArgument, scope) { const keyPath = getOnyxKeyPath(keyArgument, scope); @@ -572,33 +598,83 @@ function findRestrictedKey(keyArgument, scope) { return RESTRICTED_KEY_PATHS.has(keyPath) ? {keyPath} : null; } +function findRestrictedKeys(readMethod, call, scope) { + const keyArgument = call.arguments.at(0); + + if (readMethod !== MULTI_READ_METHOD) { + const finding = findRestrictedKey(keyArgument, scope); + + return finding ? [{node: call, ...finding}] : []; + } + + const elements = keyArgument?.type === 'SpreadElement' ? null : getKeyListElements(keyArgument, scope); + + if (!elements) { + return [{node: call, keyPath: null}]; + } + + return elements.flatMap((element) => { + if (!element || element.type === 'SpreadElement') { + return [{node: element ?? call, keyPath: null}]; + } + + const finding = findRestrictedKey(element, scope); + + return finding ? [{node: element, ...finding}] : []; + }); +} + function create(context) { const sourceCode = context.sourceCode ?? context.getSourceCode(); const filename = context.filename ?? context.getFilename(); const onyxImportBindings = new WeakSet(); - const readAliases = new WeakSet(); + const readAliases = new WeakMap(); + + function getDeclaredVariable(node, bindingName) { + return sourceCode.getDeclaredVariables(node).find((declaredVariable) => declaredVariable.name === bindingName); + } - function trackBinding(node, bindingName, bindings) { - const variable = sourceCode.getDeclaredVariables(node).find((declaredVariable) => declaredVariable.name === bindingName); + function trackImportBinding(node, bindingName) { + const variable = getDeclaredVariable(node, bindingName); if (variable) { - bindings.add(variable); + onyxImportBindings.add(variable); } + } + + function trackReadAlias(node, bindingName, readMethod) { + const variable = getDeclaredVariable(node, bindingName); - return variable; + if (variable) { + readAliases.set(variable, readMethod); + } } - function isOnyxRead(node, scope) { + function getOnyxReadMethod(node, scope) { if (node?.type !== 'MemberExpression' || node.object.type !== 'Identifier') { - return false; + return null; } - if (getStaticPropertyName(node) !== READ_METHOD) { - return false; + const propertyName = getStaticPropertyName(node); + + if (!READ_METHODS.has(propertyName)) { + return null; } const objectVariable = getVariableByName(scope, node.object.name); - return !!objectVariable && onyxImportBindings.has(objectVariable); + return !!objectVariable && onyxImportBindings.has(objectVariable) ? propertyName : null; + } + + function getCalledReadMethod(callee, scope) { + const readMethod = getOnyxReadMethod(callee, scope); + + if (readMethod) { + return readMethod; + } + + const calleeVariable = callee.type === 'Identifier' ? getVariableByName(scope, callee.name) : null; + + return calleeVariable ? (readAliases.get(calleeVariable) ?? null) : null; } return { @@ -609,7 +685,7 @@ function create(context) { for (const specifier of node.specifiers) { if (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier') { - trackBinding(node, specifier.local.name, onyxImportBindings); + trackImportBinding(node, specifier.local.name); } } }, @@ -630,8 +706,8 @@ function create(context) { const keyName = getStaticName(property.key, property.computed); - if (keyName === READ_METHOD) { - trackBinding(node, property.value.name, readAliases); + if (READ_METHODS.has(keyName)) { + trackReadAlias(node, property.value.name, keyName); } } return; @@ -645,19 +721,21 @@ function create(context) { const aliasedVariable = getVariableByName(scope, node.init.name); if (aliasedVariable && onyxImportBindings.has(aliasedVariable)) { - trackBinding(node, node.id.name, onyxImportBindings); + trackImportBinding(node, node.id.name); } } - if (isOnyxRead(node.init, scope)) { - trackBinding(node, node.id.name, readAliases); + const readMethod = getOnyxReadMethod(node.init, scope); + + if (readMethod) { + trackReadAlias(node, node.id.name, readMethod); } }, CallExpression(node) { const scope = sourceCode.getScope(node); - const calleeVariable = node.callee.type === 'Identifier' ? getVariableByName(scope, node.callee.name) : null; + const readMethod = getCalledReadMethod(node.callee, scope); - if (!isOnyxRead(node.callee, scope) && !(!!calleeVariable && readAliases.has(calleeVariable))) { + if (!readMethod) { return; } @@ -683,11 +761,11 @@ function create(context) { return; } - const finding = findRestrictedKey(node.arguments.at(0), scope); - - if (finding) { + for (const finding of findRestrictedKeys(readMethod, node, scope)) { context.report( - finding.keyPath ? {node, messageId: 'noRestrictedOnyxKey', data: {keyPath: `${ONYXKEYS_ROOT}.${finding.keyPath}`}} : {node, messageId: 'noUnresolvableOnyxKey'}, + finding.keyPath + ? {node: finding.node, messageId: 'noRestrictedOnyxKey', data: {keyPath: `${ONYXKEYS_ROOT}.${finding.keyPath}`}} + : {node: finding.node, messageId: 'noUnresolvableOnyxKey'}, ); } }, diff --git a/scripts/onyxConnectBypass.ts b/scripts/onyxConnectBypass.ts index 937f3e6b6771..30b94bee75f8 100644 --- a/scripts/onyxConnectBypass.ts +++ b/scripts/onyxConnectBypass.ts @@ -7,7 +7,7 @@ * disable directive can reach this check because it does not go through ESLint's message pipeline. * * Blanket `eslint-disable` / `eslint-disable-next-line` with no rule list counts only when it - * covers a real banned call: Onyx.connect(), or Onyx.get() for the read rule. Unrelated blanket comments (e.g. around ReportUtils) remain + * covers a real banned call: Onyx.connect(), or Onyx.get() and Onyx.multiGet() for the read rule. Unrelated blanket comments (e.g. around ReportUtils) remain * ignored. Call sites are found via the Babel AST so comments and grouping parens cannot hide a * banned member access from a source scan. */ @@ -59,12 +59,12 @@ const ONYX_READ_BAN: BannedRule = { id: 'rulesdir/no-unsafe-onyx-read', name: 'no-unsafe-onyx-read', objects: new Set(['Onyx']), - methods: new Set(['get']), + methods: new Set(['get', 'multiGet']), grandfathered: new Map([['src/setup/addUtilsToWindow.ts', 1]]), appliesTo: (file) => file.startsWith('src/'), searchTerms: ['Onyx', 'eslint-disable'], message: - 'Onyx reads checked by no-unsafe-onyx-read cannot be silenced with eslint-disable. Fix the read instead: use useOnyx() for data a component renders or reacts to, and call Onyx.get() only from event handlers or useCallback bodies in components, pages and hooks.', + 'Onyx reads checked by no-unsafe-onyx-read cannot be silenced with eslint-disable. Fix the read instead: use useOnyx() for data a component renders or reacts to, and call Onyx.get() or Onyx.multiGet() only from event handlers or useCallback bodies in components, pages and hooks.', }; const BANNED_RULES: BannedRule[] = [ONYX_CONNECT_BAN, ONYX_READ_BAN]; diff --git a/tests/unit/NoUnsafeOnyxReadRuleTest.ts b/tests/unit/NoUnsafeOnyxReadRuleTest.ts index 662e2f16121e..721a79ca03bb 100644 --- a/tests/unit/NoUnsafeOnyxReadRuleTest.ts +++ b/tests/unit/NoUnsafeOnyxReadRuleTest.ts @@ -444,3 +444,45 @@ describe('no-unsafe-onyx-read allowed paths', () => { ], }); }); + +describe('no-unsafe-onyx-read multiGet', () => { + ruleTester.run(ruleModule.name, ruleModule, { + valid: [ + `${ONYX_IMPORT} function Row() { const onPress = async () => { const [session, account] = await Onyx.multiGet([ONYXKEYS.SESSION, ONYXKEYS.ACCOUNT]); submit(session, account); }; return ; }`, + `${ONYX_IMPORT} export function submit(id) { return Onyx.multiGet([ONYXKEYS.SESSION, \`\${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}\${id}\`]); }`, + `${ONYX_IMPORT} export function submit() { const keys = [ONYXKEYS.SESSION, ONYXKEYS.ACCOUNT]; return Onyx.multiGet(keys); }`, + `${ONYX_IMPORT} export function submit() { return Onyx.multiGet([]); }`, + + 'const Onyx = {multiGet: () => []}; function Row() { const value = Onyx.multiGet([ONYXKEYS.SESSION]); return ; }', + ], + invalid: [ + {code: `${ONYX_IMPORT} function Row() { const values = use(Onyx.multiGet([ONYXKEYS.SESSION])); return ; }`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} const {multiGet} = Onyx; const initialValues = multiGet([ONYXKEYS.SESSION]);`, errors: MODULE_SCOPE_ERRORS}, + {code: `${ONYX_IMPORT} const readMany = Onyx.multiGet; function Row() { const values = readMany([ONYXKEYS.SESSION]); return ; }`, errors: RENDER_ERRORS}, + + {code: `${ONYX_IMPORT} export function submit() { return Onyx.multiGet([ONYXKEYS.SESSION, ONYXKEYS.COLLECTION.REPORT]); }`, errors: RESTRICTED_ERRORS}, + { + code: `${ONYX_IMPORT} export function submit(reportID) { return Onyx.multiGet([ONYXKEYS.PERSONAL_DETAILS_LIST, \`\${ONYXKEYS.COLLECTION.REPORT}\${reportID}\`]); }`, + errors: [{messageId: 'noRestrictedOnyxKey'}, {messageId: 'noRestrictedOnyxKey'}], + }, + {code: `${ONYX_IMPORT} export function submit() { const keys = [ONYXKEYS.COLLECTION.REPORT]; return Onyx.multiGet(keys); }`, errors: RESTRICTED_ERRORS}, + {code: `${ONYX_IMPORT} const {multiGet} = Onyx; export function submit() { return multiGet([ONYXKEYS.COLLECTION.REPORT]); }`, errors: RESTRICTED_ERRORS}, + + {code: `${ONYX_IMPORT} export function submit(keys) { return Onyx.multiGet(keys); }`, errors: UNRESOLVABLE_ERRORS}, + {code: `${ONYX_IMPORT} export function submit(key) { return Onyx.multiGet([ONYXKEYS.SESSION, key]); }`, errors: UNRESOLVABLE_ERRORS}, + {code: `${ONYX_IMPORT} export function submit(keys) { return Onyx.multiGet([ONYXKEYS.SESSION, ...keys]); }`, errors: UNRESOLVABLE_ERRORS}, + {code: `${ONYX_IMPORT} export function submit(ids) { return Onyx.multiGet(ids.map((id) => \`\${ONYXKEYS.COLLECTION.POLICY_TAGS}\${id}\`)); }`, errors: UNRESOLVABLE_ERRORS}, + ], + }); +}); + +describe('no-unsafe-onyx-read multiGet under the TypeScript parser', () => { + tsRuleTester.run(ruleModule.name, ruleModule, { + valid: [ + {code: `${ONYX_IMPORT} export function submit() { return Onyx.multiGet([ONYXKEYS.SESSION, ONYXKEYS.ACCOUNT] as const); }`}, + {code: `${ONYX_IMPORT} export function submit() { const keys = [ONYXKEYS.SESSION, ONYXKEYS.ACCOUNT] as const; return Onyx.multiGet(keys); }`}, + ], + invalid: [{code: `${ONYX_IMPORT} export function submit() { return Onyx.multiGet([ONYXKEYS.SESSION, ONYXKEYS.COLLECTION.REPORT] as const); }`, errors: RESTRICTED_ERRORS}], + }); +}); diff --git a/tests/unit/OnyxConnectBypassTest.ts b/tests/unit/OnyxConnectBypassTest.ts index fa1e2baeb8a9..4c92015f968e 100644 --- a/tests/unit/OnyxConnectBypassTest.ts +++ b/tests/unit/OnyxConnectBypassTest.ts @@ -165,6 +165,15 @@ describe('no-unsafe-onyx-read bypasses', () => { expect(collectDisableDirectivesFromSource(source, 'src/libs/Foo.ts', ONYX_READ_BAN)).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); }); + it('flags a blanket disable that covers an Onyx.multiGet call', () => { + // Given a blanket disable over a multi-key read + const source = ['/* eslint-disable */', `await Onyx${'.multiGet'}([ONYXKEYS.SESSION]);`].join('\n'); + + // When the read ban scans it + // Then it counts, because the rule checks multiGet the same way it checks get + expect(collectDisableDirectivesFromSource(source, 'src/libs/Foo.ts', ONYX_READ_BAN)).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); + }); + it('ignores a blanket disable that covers no read', () => { // Given a blanket disable over code that never reads Onyx const source = ['/* eslint-disable */', 'console.log(1);'].join('\n'); From aa0f618d36efef4456a4d1678d247354673c0b7b Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Fri, 25 Sep 2026 10:36:26 +0200 Subject: [PATCH 3/7] Block eslint-disable over runtime OnyxUtils imports in the bypass check --- .../onyx-1-no-render-reachable-onyx-read.md | 2 +- scripts/checkOnyxConnectBypass.ts | 3 +- scripts/onyxConnectBypass.ts | 82 +++++++++++++++---- tests/unit/OnyxConnectBypassTest.ts | 71 +++++++++++++++- 4 files changed, 141 insertions(+), 17 deletions(-) diff --git a/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md b/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md index 485bad722e88..2d1e33cd8fc7 100644 --- a/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md +++ b/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md @@ -18,7 +18,7 @@ Do not re-check these: | A read inside an effect, or in a same-file function an effect calls | `no-unsafe-onyx-read` | | A Search snapshot key, or a key lint can't resolve, including any `multiGet` element | `no-unsafe-onyx-read` | | A runtime import of `react-native-onyx/dist/OnyxUtils` | `@typescript-eslint/no-restricted-imports` | -| An inline `eslint-disable` of the rule | `scripts/checkOnyxConnectBypass.ts` | +| An inline `eslint-disable` of the rule, or one over a runtime OnyxUtils import | `scripts/checkOnyxConnectBypass.ts` | | A missing `await` whose value is then used | `tsc` | What's left crosses a file boundary, depends on write ordering, only shows in the diff, or happens after the read. diff --git a/scripts/checkOnyxConnectBypass.ts b/scripts/checkOnyxConnectBypass.ts index 3acde839b457..4aa50f348bc8 100644 --- a/scripts/checkOnyxConnectBypass.ts +++ b/scripts/checkOnyxConnectBypass.ts @@ -2,7 +2,8 @@ import {file} from 'bun'; /** - * Fails the lint run when a new inline `eslint-disable` bypasses the Onyx.connect() ban or `rulesdir/no-unsafe-onyx-read`. + * Fails the lint run when a new inline `eslint-disable` silences one of the bans in `BANNED_RULES` + * (see `onyxConnectBypass.ts`). * * The ban (`rulesdir/no-onyx-connect`, shipped by eslint-config-expensify) is a normal lint rule, * so an inline disable can silence it. The runner re-elevates those disables by scanning source diff --git a/scripts/onyxConnectBypass.ts b/scripts/onyxConnectBypass.ts index 30b94bee75f8..f157bb674740 100644 --- a/scripts/onyxConnectBypass.ts +++ b/scripts/onyxConnectBypass.ts @@ -1,15 +1,15 @@ /** - * Detection logic for new `eslint-disable` bypasses of the Onyx.connect() ban and `rulesdir/no-unsafe-onyx-read`. + * Finds `eslint-disable` directives that silence one of the bans in `BANNED_RULES`: + * `rulesdir/no-onyx-connect`, `rulesdir/no-unsafe-onyx-read`, and the + * `@typescript-eslint/no-restricted-imports` entry for `react-native-onyx/dist/OnyxUtils`. * - * `rulesdir/no-onyx-connect` (shipped by eslint-config-expensify) is a normal lint rule, so an - * inline `eslint-disable` can silence it. The lint runner re-elevates those disables by scanning - * source for disable directives that name the ban or blanket directives that cover a real call. No - * disable directive can reach this check because it does not go through ESLint's message pipeline. + * A directive counts when it names the ban's rule, or when it is a blanket disable over a banned + * call: `Onyx.connect()` for the connect ban, `Onyx.get()` or `Onyx.multiGet()` for the read ban. + * For the OnyxUtils ban it counts only when it covers a runtime import of the module. Blanket + * disables over unrelated code, like the ones around ReportUtils, are ignored. * - * Blanket `eslint-disable` / `eslint-disable-next-line` with no rule list counts only when it - * covers a real banned call: Onyx.connect(), or Onyx.get() and Onyx.multiGet() for the read rule. Unrelated blanket comments (e.g. around ReportUtils) remain - * ignored. Call sites are found via the Babel AST so comments and grouping parens cannot hide a - * banned member access from a source scan. + * Calls and imports are located in the Babel AST, so spacing, comments and parentheses cannot hide + * them the way they could from a text scan. */ import {parse} from '@babel/parser'; @@ -38,6 +38,8 @@ type BannedRule = { name: string; objects: Set; methods: Set; + /** Bans runtime imports of these modules instead of `objects`/`methods` calls. */ + importSources?: Set; grandfathered: Map; appliesTo: (file: string) => boolean; searchTerms: string[]; @@ -67,7 +69,20 @@ const ONYX_READ_BAN: BannedRule = { 'Onyx reads checked by no-unsafe-onyx-read cannot be silenced with eslint-disable. Fix the read instead: use useOnyx() for data a component renders or reacts to, and call Onyx.get() or Onyx.multiGet() only from event handlers or useCallback bodies in components, pages and hooks.', }; -const BANNED_RULES: BannedRule[] = [ONYX_CONNECT_BAN, ONYX_READ_BAN]; +const ONYX_UTILS_IMPORT_BAN: BannedRule = { + id: '@typescript-eslint/no-restricted-imports', + name: '@typescript-eslint/no-restricted-imports', + objects: new Set(), + methods: new Set(), + importSources: new Set(['react-native-onyx/dist/OnyxUtils']), + grandfathered: new Map(), + appliesTo: (file) => file.startsWith('src/'), + searchTerms: ['OnyxUtils', 'eslint-disable'], + message: + 'The react-native-onyx/dist/OnyxUtils import restriction cannot be silenced with eslint-disable. Read Onyx with useOnyx(), Onyx.get() or Onyx.multiGet() instead. Type-only imports of OnyxUtils are still allowed.', +}; + +const BANNED_RULES: BannedRule[] = [ONYX_CONNECT_BAN, ONYX_READ_BAN, ONYX_UTILS_IMPORT_BAN]; /** A banned-rule violation that an inline disable directive silenced. */ type SuppressedBan = { @@ -186,6 +201,44 @@ function collectBannedCallOffsets(root: ASTNode, ban: BannedRule): number[] { return offsets; } +function isTypeOnlyImport(node: ASTNode): boolean { + if (node.importKind === 'type' || node.exportKind === 'type') { + return true; + } + const specifiers = node.specifiers; + return ( + Array.isArray(specifiers) && + specifiers.length > 0 && + specifiers.every((specifier) => BabelASTUtils.isRecord(specifier) && (specifier.importKind === 'type' || specifier.exportKind === 'type')) + ); +} + +function importedSource(node: ASTNode): string | null { + if (node.type === 'ImportDeclaration' || node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') { + return BabelASTUtils.isASTNode(node.source) && typeof node.source.value === 'string' ? node.source.value : null; + } + if (node.type === 'TSImportEqualsDeclaration' && BabelASTUtils.isASTNode(node.moduleReference) && node.moduleReference.type === 'TSExternalModuleReference') { + const expression = node.moduleReference.expression; + return BabelASTUtils.isASTNode(expression) && typeof expression.value === 'string' ? expression.value : null; + } + return null; +} + +function collectBannedImportOffsets(root: ASTNode, sources: Set): number[] { + const offsets: number[] = []; + const visit = (node: ASTNode) => { + const source = importedSource(node); + if (source !== null && sources.has(source) && !isTypeOnlyImport(node)) { + offsets.push(node.start); + } + for (const child of BabelASTUtils.children(node, NON_CHILD_KEYS)) { + visit(child); + } + }; + visit(root); + return offsets; +} + function normalizedDirectiveArgs(args: string): string { return args .replace(/--[\s\S]*$/, '') @@ -265,13 +318,14 @@ function collectDisableDirectivesFromSource(source: string, file: string, ban: B return []; } const bans: SuppressedBan[] = []; - const callOffsets = collectBannedCallOffsets(parsed.root, ban); + const callOffsets = ban.importSources ? collectBannedImportOffsets(parsed.root, ban.importSources) : collectBannedCallOffsets(parsed.root, ban); const enableMatches = collectDirectiveMatches(parsed.comments, source, 'enable'); for (const match of collectDirectiveMatches(parsed.comments, source, 'disable')) { const args = directiveArgs(match); const targetsBan = directiveTargetsBan(args, ban); - const coversBan = isBlanketDirective(args) && blanketDirectiveCoversCall(source, match, callOffsets, enableMatches, ban); - if (!targetsBan && !coversBan) { + const isBlanket = isBlanketDirective(args); + const coversBan = (isBlanket || (!!ban.importSources && targetsBan)) && blanketDirectiveCoversCall(source, match, callOffsets, enableMatches, ban); + if ((ban.importSources || !targetsBan) && !coversBan) { continue; } const prefix = source.slice(0, match.index); @@ -302,5 +356,5 @@ function findNewBypasses(suppressedBans: readonly SuppressedBan[], rule: BannedR return newBypasses; } -export {BANNED_RULE_ID, BANNED_RULE_NAME, BANNED_RULES, GRANDFATHERED_BYPASSES, ONYX_CONNECT_BAN, ONYX_READ_BAN, collectDisableDirectivesFromSource, findNewBypasses}; +export {BANNED_RULE_ID, BANNED_RULE_NAME, BANNED_RULES, GRANDFATHERED_BYPASSES, ONYX_CONNECT_BAN, ONYX_READ_BAN, ONYX_UTILS_IMPORT_BAN, collectDisableDirectivesFromSource, findNewBypasses}; export type {BannedRule, SuppressedBan}; diff --git a/tests/unit/OnyxConnectBypassTest.ts b/tests/unit/OnyxConnectBypassTest.ts index 4c92015f968e..c50d01369a48 100644 --- a/tests/unit/OnyxConnectBypassTest.ts +++ b/tests/unit/OnyxConnectBypassTest.ts @@ -1,4 +1,4 @@ -import {BANNED_RULE_ID, ONYX_READ_BAN, collectDisableDirectivesFromSource, findNewBypasses} from '../../scripts/onyxConnectBypass'; +import {BANNED_RULE_ID, ONYX_READ_BAN, ONYX_UTILS_IMPORT_BAN, collectDisableDirectivesFromSource, findNewBypasses} from '../../scripts/onyxConnectBypass'; const ONYX_CONNECT_CALL = `Onyx${'.connect'}`; const onyxConnectCall = (key: string): string => `${ONYX_CONNECT_CALL}({key: "${key}"});`; @@ -216,3 +216,72 @@ describe('no-unsafe-onyx-read bypasses', () => { expect(ONYX_READ_BAN.appliesTo('tests/unit/FooTest.ts')).toBe(false); }); }); + +describe('OnyxUtils import bypasses', () => { + const onyxUtilsImport = `import OnyxUtils from 'react-native-onyx/dist/${'OnyxUtils'}';`; + const scan = (lines: string[]) => collectDisableDirectivesFromSource(lines.join('\n'), 'src/libs/Foo.ts', ONYX_UTILS_IMPORT_BAN); + + it('flags a disable of the restricted-imports rule over an OnyxUtils import', () => { + // Given a src file that silences the import restriction on the OnyxUtils import + // When the OnyxUtils ban scans it + // Then the directive counts as a bypass, since OnyxUtils must not be imported at runtime + expect(scan(['// eslint-disable-next-line @typescript-eslint/no-restricted-imports', onyxUtilsImport])).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); + expect(scan([`${onyxUtilsImport} // eslint-disable-line @typescript-eslint/no-restricted-imports`])).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); + expect(scan(['/* eslint-disable @typescript-eslint/no-restricted-imports */', onyxUtilsImport])).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); + }); + + it('flags a blanket disable over an OnyxUtils import', () => { + // Given a blanket disable over the import + // When the OnyxUtils ban scans it + // Then it counts, because a blanket directive silences the restriction just as well + expect(scan(['// eslint-disable-next-line', onyxUtilsImport])).toEqual([{file: 'src/libs/Foo.ts', line: 1}]); + }); + + it('flags re-exports and import-equals of OnyxUtils', () => { + // Given other runtime forms the restriction also reports + // When the OnyxUtils ban scans them + // Then each disable counts, so the ban cannot be dodged by changing the import syntax + expect(scan(['// eslint-disable-next-line @typescript-eslint/no-restricted-imports', `export {default} from 'react-native-onyx/dist/${'OnyxUtils'}';`])).toEqual([ + {file: 'src/libs/Foo.ts', line: 1}, + ]); + expect(scan(['// eslint-disable-next-line @typescript-eslint/no-restricted-imports', `import OnyxUtils = require('react-native-onyx/dist/${'OnyxUtils'}');`])).toEqual([ + {file: 'src/libs/Foo.ts', line: 1}, + ]); + }); + + it('ignores restricted-imports disables over other modules', () => { + // Given the many existing disables that silence the rule for unrelated paths + const source = [ + '// eslint-disable-next-line @typescript-eslint/no-restricted-imports', + "import {Text} from 'react-native';", + onyxUtilsImport.replace('import OnyxUtils', 'import type OnyxUtils'), + ]; + + // When the OnyxUtils ban scans it + // Then nothing is reported, so only OnyxUtils imports are locked down + expect(scan(source)).toEqual([]); + }); + + it('ignores type-only OnyxUtils imports', () => { + // Given type-only imports, which the restriction allows + // When the OnyxUtils ban scans a disable above each + // Then nothing is reported, since there is no runtime import to protect + expect(scan(['// eslint-disable-next-line', `import type {OnyxKey} from 'react-native-onyx/dist/${'OnyxUtils'}';`])).toEqual([]); + expect(scan(['// eslint-disable-next-line', `import {type OnyxKey} from 'react-native-onyx/dist/${'OnyxUtils'}';`])).toEqual([]); + }); + + it('ignores a disable that names a different rule', () => { + // Given a disable of another rule over the import + // When the OnyxUtils ban scans it + // Then nothing is reported, because that directive cannot silence the import restriction + expect(scan(['// eslint-disable-next-line no-console', onyxUtilsImport])).toEqual([]); + }); + + it('covers src but not tests', () => { + // Given a src file and a test suite + // When the OnyxUtils ban decides which files it checks + // Then tests are out of scope, matching the Onyx read ban + expect(ONYX_UTILS_IMPORT_BAN.appliesTo('src/Expensify.tsx')).toBe(true); + expect(ONYX_UTILS_IMPORT_BAN.appliesTo('tests/unit/FooTest.ts')).toBe(false); + }); +}); From 0b47fe5d9278c54d06300c6d553eb1fd1837fa52 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Fri, 25 Sep 2026 11:16:56 +0200 Subject: [PATCH 4/7] Add workflow requesting Onyx reviewers for new Onyx.get and Onyx.multiGet calls --- .github/workflows/onyxGetReviewers.yml | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/onyxGetReviewers.yml diff --git a/.github/workflows/onyxGetReviewers.yml b/.github/workflows/onyxGetReviewers.yml new file mode 100644 index 000000000000..c8a4b3e536d1 --- /dev/null +++ b/.github/workflows/onyxGetReviewers.yml @@ -0,0 +1,89 @@ +name: Onyx.get reviewers + +# Requests a review from the Onyx performance reviewers whenever a PR adds a new +# `Onyx.get` or `Onyx.multiGet` call. CODEOWNERS can't express this because it triggers on file +# paths, not on the content of a diff. + +on: + pull_request_target: + types: [opened, synchronize, ready_for_review] + +permissions: + contents: read + pull-requests: write + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + requestReviewers: + name: Request reviewers for new Onyx.get and Onyx.multiGet calls + if: ${{ github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + steps: + - name: Request reviewers and leave a comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7 + with: + script: | + const REVIEWERS = ['tgolen', 'mountiny', 'chuckdries']; + const pr = context.payload.pull_request; + const {owner, repo} = context.repo; + + // Trigger only on a net-new production call. Only code under `src/` counts, since docs, lint rules + // and scripts mention `Onyx.get(` in prose and messages. Jest mocks are excluded, and we + // compare added vs. removed call lines per file (a call matches `Onyx.get(` or `Onyx.multiGet(`, + // but not `Onyx.getAllKeys(` or the dev-console `window.Onyx.get(`), so editing a call in place + // nets to zero, lines that start with a comment don't count on either side, and a removal in one + // file can't mask a new call in another. + const CALL = /(? { + if (!file.patch || !SOURCE_FILE.test(file.filename) || MOCK_DIRECTORY.test(file.filename)) { + return false; + } + let added = 0; + let removed = 0; + for (const line of file.patch.split('\n')) { + const code = line.slice(1); + if (COMMENT_LINE.test(code) || !CALL.test(code)) { + continue; + } + if (line.startsWith('+') && !line.startsWith('+++')) { + added++; + } else if (line.startsWith('-') && !line.startsWith('---')) { + removed++; + } + } + return added > removed; + }); + + if (!addsNewCall) { + return; + } + + // Don't request the PR author on their own PR, anyone already requested, or anyone who + // already reviewed, so re-runs on `synchronize` never create duplicate requests. + const alreadyRequested = (pr.requested_reviewers ?? []).map((user) => user.login); + const reviews = await github.paginate(github.rest.pulls.listReviews, {owner, repo, pull_number: pr.number}); + const alreadyReviewed = reviews.map((review) => review.user?.login).filter(Boolean); + const skip = new Set([pr.user.login, ...alreadyRequested, ...alreadyReviewed]); + const reviewersToRequest = REVIEWERS.filter((reviewer) => !skip.has(reviewer)); + + if (reviewersToRequest.length === 0) { + return; + } + + await github.rest.pulls.requestReviewers({owner, repo, pull_number: pr.number, reviewers: reviewersToRequest}); + + const mentions = REVIEWERS.map((reviewer) => `@${reviewer}`).join(', '); + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr.number, + body: `This PR adds a new \`Onyx.get\` or \`Onyx.multiGet\` call, so I've requested a review from the Onyx performance reviewers (${mentions}). A review from any one of them is enough. Please check the call against the \`Onyx.get()\` rules in [ONYX-DATA-MANAGEMENT](https://github.com/Expensify/App/blob/main/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md) and [ONYX-1](https://github.com/Expensify/App/blob/main/.claude/skills/app-coding-standards/rules/onyx-1-no-render-reachable-onyx-read.md).`, + }); From c3c5b3731e868358f3972c4dda4572f79e551863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 29 Sep 2026 14:27:28 +0100 Subject: [PATCH 5/7] Use newest Onyx changes --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0771deafb003..e890d1fa6038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,7 +126,7 @@ "react-native-nitro-fetch": "1.6.2", "react-native-nitro-modules": "0.37.1", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b3c781503af7ba879149c028cc0c36523e8a7259", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b548d5b6b8d304444948765c389725e7ac937fa1", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -37879,9 +37879,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.115", - "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#b3c781503af7ba879149c028cc0c36523e8a7259", - "integrity": "sha512-r+4E5BTRQe1FcaJZrlcx09gle+seMBZDeVCbysnKLkhRGIrCt/GmuKNO92dLctFypvtKY5ecyXnhLEsv3KaIwg==", + "version": "3.0.116", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#b548d5b6b8d304444948765c389725e7ac937fa1", + "integrity": "sha512-VI0/dE6TPjt5ezG5P7k3VqKUX+U8QNaArx505n+vl0pm+m77TiU1BsNt7ikeh6Lju7GPURUXk3iAoBG+sziBUw==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index bcf05e3fc6d5..bdcb6ee9d114 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "react-native-nitro-fetch": "1.6.2", "react-native-nitro-modules": "0.37.1", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b3c781503af7ba879149c028cc0c36523e8a7259", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b548d5b6b8d304444948765c389725e7ac937fa1", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", From c7ffe48ed2855369b41a4ddfde85def4fa1871f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 29 Sep 2026 14:42:09 +0100 Subject: [PATCH 6/7] Apply review and update doc --- .github/workflows/onyxGetReviewers.yml | 2 +- contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/onyxGetReviewers.yml b/.github/workflows/onyxGetReviewers.yml index c8a4b3e536d1..952395b560cd 100644 --- a/.github/workflows/onyxGetReviewers.yml +++ b/.github/workflows/onyxGetReviewers.yml @@ -27,7 +27,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7 with: script: | - const REVIEWERS = ['tgolen', 'mountiny', 'chuckdries']; + const REVIEWERS = ['tgolen', 'mountiny', 'chuckdries', 'luacmartins']; const pr = context.payload.pull_request; const {owner, repo} = context.repo; diff --git a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md index 45ab5cc7ef0e..086c831de708 100644 --- a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md +++ b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md @@ -42,7 +42,7 @@ There are four ways to read Onyx data, and `Onyx.connect` is deprecated: 1. **`useOnyx`** (from `@hooks/useOnyx`) — the default for anything a React component renders. 2. **`Onyx.connectWithoutView`** — an imperative subscription for non-render logic, used only when `useOnyx` genuinely does not fit. 3. **`Onyx.get()`**: an asynchronous, one-shot read of the cache that never subscribes, for event handlers in components, pages and hooks. -4. **`Onyx.multiGet()`**: a thin wrapper around `Onyx.get()` that calls it for each key in an array and resolves the values in the same order. Every `Onyx.get()` rule below applies to it and to each key it reads. +4. **`Onyx.multiGet()`**: takes an array of keys and resolves each one the way `Onyx.get()` does, in the order given. Every `Onyx.get()` rule below applies to it and to each key it reads. ### - Prefer a pure function over reading Onyx at all A pure function does not read Onyx itself — it receives the data it needs as parameters, and its caller does the reading (with `useOnyx` or `Onyx.connectWithoutView`) and passes it in. Before adding either subscription, check whether the code can be a pure function instead: it needs no connection, is trivial to test, and cannot cause extra rerenders. Prefer this even when it means passing more arguments. This takes precedence over everything below. From a8691ebbf2f0c84b54192426bcb52f4c2321b561 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 7 Oct 2026 08:46:56 +0200 Subject: [PATCH 7/7] Update react-native-onyx to 3.0.119 --- package-lock.json | 885 +++++++++++++++++++++------------------------- package.json | 36 +- 2 files changed, 423 insertions(+), 498 deletions(-) diff --git a/package-lock.json b/package-lock.json index e890d1fa6038..ae4564f9f25c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.4.92-0", + "version": "9.5.5-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.4.92-0", + "version": "9.5.5-0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -21,7 +21,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.336", + "@expensify/react-native-live-markdown": "0.1.342", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", @@ -62,12 +62,13 @@ "array.prototype.tosorted": "^1.1.4", "awesome-phonenumber": "^5.4.0", "canvas-size": "^1.2.6", + "canvaskit-wasm": "0.41.0", "d3-scale": "^4.0.2", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.201", + "expensify-common": "2.0.207", "expo": "57.0.8", "expo-asset": "57.0.7", "expo-audio": "57.0.3", @@ -89,11 +90,11 @@ "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", "json5": "2.2.2", - "lodash-es": "4.17.21", + "lodash-es": "4.18.1", "lottie-react-native": "7.3.8", "mapbox-gl": "^3.24.0", - "metro": "0.84.4", - "metro-transform-plugins": "0.84.4", + "metro": "0.84.5", + "metro-transform-plugins": "0.84.5", "onfido-sdk-ui": "14.53.1", "pako": "^2.1.0", "process": "^0.11.10", @@ -115,7 +116,7 @@ "react-native-device-info": "10.3.1", "react-native-draggable-flatlist": "^4.0.3", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "2.32.0", + "react-native-gesture-handler": "3.3.0", "react-native-google-places-autocomplete": "2.6.4", "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.1.2", @@ -125,8 +126,8 @@ "react-native-localize": "^3.5.4", "react-native-nitro-fetch": "1.6.2", "react-native-nitro-modules": "0.37.1", - "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b548d5b6b8d304444948765c389725e7ac937fa1", + "react-native-nitro-sqlite": "9.8.3", + "react-native-onyx": "3.0.119", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -137,8 +138,8 @@ "react-native-quick-crypto": "^1.1.6", "react-native-reanimated": "4.5.5", "react-native-render-html": "6.3.1", - "react-native-safe-area-context": "5.6.2", - "react-native-screens": "4.25.0", + "react-native-safe-area-context": "5.9.1", + "react-native-screens": "4.28.0", "react-native-share": "11.0.2", "react-native-svg": "15.15.5", "react-native-tab-view": "^4.3.0", @@ -153,7 +154,7 @@ "react-webcam": "^7.1.1", "scheduler": "0.27.0", "victory-native": "^41.21.0", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { "@aaroon/workbox-rspack-plugin": "^1.0.1", @@ -2912,14 +2913,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", + "version": "7.29.4", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -4353,22 +4354,6 @@ "node": ">=8" } }, - "node_modules/@callstack/repack/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "dev": true, - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/@callstack/repack/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -5219,16 +5204,6 @@ "url": "https://github.com/sponsors/dword-design" } }, - "node_modules/@egjs/hammerjs": { - "version": "2.0.17", - "license": "MIT", - "dependencies": { - "@types/hammerjs": "^2.0.36" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@emnapi/core": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", @@ -5882,10 +5857,20 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5951,19 +5936,16 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.336", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.336.tgz", - "integrity": "sha512-rrbcQmiTNdv7iySVbQWoY7wMGpIf9e+Wx7LJkx23AGkGwAkq7Mfptsly/4a6GaunIQ3Qh5+D84cLxlI42rLlWQ==", + "version": "0.1.342", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.342.tgz", + "integrity": "sha512-xt8t0pUTHHPfY9G25wZ0rQUP44eFdVQp44Mo2Ji2H66zr/qjUnDqDOQPJxi5azucUwI/uORmmMJOvXMdKsEDNg==", "license": "MIT", "workspaces": [ "./example", "./WebExample" ], - "engines": { - "node": ">= 18.0.0" - }, "peerDependencies": { - "expensify-common": ">=2.0.189", + "expensify-common": ">=2.0.207", "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" @@ -7496,25 +7478,25 @@ } }, "node_modules/@expo/metro": { - "version": "56.0.0", - "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.0.tgz", - "integrity": "sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A==", - "license": "MIT", - "dependencies": { - "metro": "0.84.4", - "metro-babel-transformer": "0.84.4", - "metro-cache": "0.84.4", - "metro-cache-key": "0.84.4", - "metro-config": "0.84.4", - "metro-core": "0.84.4", - "metro-file-map": "0.84.4", - "metro-minify-terser": "0.84.4", - "metro-resolver": "0.84.4", - "metro-runtime": "0.84.4", - "metro-source-map": "0.84.4", - "metro-symbolicate": "0.84.4", - "metro-transform-plugins": "0.84.4", - "metro-transform-worker": "0.84.4" + "version": "56.0.2", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.2.tgz", + "integrity": "sha512-Ld5AeYMCCDa8bLeWhfuLbZFFjlV3f6ORqyPz2glGh6RltIngMuLf9BTC2yvHFjkKuGxL5SynijmA8xmNNWn5iA==", + "license": "MIT", + "dependencies": { + "metro": "0.84.5", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-config": "0.84.5", + "metro-core": "0.84.5", + "metro-file-map": "0.84.5", + "metro-minify-terser": "0.84.5", + "metro-resolver": "0.84.5", + "metro-runtime": "0.84.5", + "metro-source-map": "0.84.5", + "metro-symbolicate": "0.84.5", + "metro-transform-plugins": "0.84.5", + "metro-transform-worker": "0.84.5" } }, "node_modules/@expo/metro-config": { @@ -8507,9 +8489,9 @@ } }, "node_modules/@expo/xcpretty/node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -12528,6 +12510,19 @@ "eslint-scope": "5.1.1" } }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "devOptional": true, @@ -14370,29 +14365,24 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", + "version": "2.0.5", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", + "version": "1.1.1", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", + "version": "1.1.1", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { "version": "1.0.2", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "license": "BSD-3-Clause" @@ -14402,7 +14392,7 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", + "version": "1.1.1", "license": "BSD-3-Clause" }, "node_modules/@pusher/pusher-websocket-react-native": { @@ -14565,10 +14555,20 @@ } }, "node_modules/@react-native-community/cli-config/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -16126,9 +16126,9 @@ } }, "node_modules/@rsbuild/plugin-svgr/node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -18230,6 +18230,23 @@ "devOptional": true, "license": "BSD-3-Clause" }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.4.tgz", + "integrity": "sha512-EtMX6XjRWSastG2SdkmSQByPtJ9dx/NIjnHbpQPCYt62j4RmSx5rgLTGpw0YCjF5h191gZOmBfheOT23cRSFdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "license": "MIT" @@ -19968,10 +19985,6 @@ "@types/node": "*" } }, - "node_modules/@types/hammerjs": { - "version": "2.0.41", - "license": "MIT" - }, "node_modules/@types/howler": { "version": "2.2.12", "dev": true, @@ -21564,23 +21577,14 @@ "node": ">=0.4.0" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/adm-zip": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", - "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/agent-base": { @@ -21737,6 +21741,19 @@ "node": ">= 8" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/app-root-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", @@ -22444,7 +22461,9 @@ } }, "node_modules/babel-plugin-module-resolver/node_modules/minimatch": { - "version": "5.1.6", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -22698,9 +22717,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.42", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", - "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -22917,9 +22936,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "funding": [ { "type": "opencollective", @@ -22936,11 +22955,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -22969,40 +22988,6 @@ "browserslist-to-es-version": "dist/cli.js" } }, - "node_modules/browserslist-to-es-version/node_modules/browserslist": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", - "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001800", - "electron-to-chromium": "^1.5.387", - "node-releases": "^2.0.50", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/bser": { "version": "2.1.1", "license": "Apache-2.0", @@ -23227,9 +23212,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001803", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", - "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "funding": [ { "type": "opencollective", @@ -23269,19 +23254,6 @@ "node": ">=4" } }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -23670,15 +23642,6 @@ "dev": true, "license": "MIT" }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -23966,19 +23929,6 @@ "node": ">=8" } }, - "node_modules/concurrently/node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/concurrently/node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -24101,39 +24051,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-compat/node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -24160,16 +24077,6 @@ "node": ">= 0.4.0" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -24435,9 +24342,9 @@ } }, "node_modules/cspell-glob/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -25465,9 +25372,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.389", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", - "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", "license": "ISC" }, "node_modules/emittery": { @@ -26967,9 +26874,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.201", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.201.tgz", - "integrity": "sha512-8ziun4VC5bcQobhb+rmpCuaAiXBapPrxiAUyITt6lLaYBa1K2pfkcvZypgCmrj3eLselOvAVkf5Y/dVDcWs6Cg==", + "version": "2.0.207", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.207.tgz", + "integrity": "sha512-AHrgp9LZkS9z/37G5ZbKbIwa9GqSCPVwfwilS1yhSQmFyL6Jb/eDxlopMfthaTbXO0NHTPRH2UmImlUtdlNYvw==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", @@ -26989,9 +26896,9 @@ } }, "node_modules/expensify-common/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -27783,22 +27690,42 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-parser": { - "version": "4.4.1", + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", "devOptional": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", + "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", + "devOptional": true, + "funding": [ { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" } ], "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -28298,9 +28225,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -28458,17 +28385,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -28514,15 +28441,6 @@ "node": ">= 0.6" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -28964,10 +28882,12 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -29273,7 +29193,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -29317,17 +29239,6 @@ "hermes-estree": "0.36.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -29697,20 +29608,22 @@ } }, "node_modules/image-size": { - "version": "1.1.1", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.4.tgz", + "integrity": "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw==", + "dev": true, "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, "engines": { - "node": ">=16.x" + "node": ">=18" } }, "node_modules/immediate": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/import-fresh": { @@ -30347,6 +30260,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -32855,7 +32781,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -33399,19 +33327,6 @@ "shell-quote": "^1.8.4" } }, - "node_modules/launch-editor/node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/lefthook": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.1.9.tgz", @@ -33966,7 +33881,9 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.bindall": { @@ -34484,9 +34401,9 @@ } }, "node_modules/metro": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.4.tgz", - "integrity": "sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.5.tgz", + "integrity": "sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -34504,23 +34421,22 @@ "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", - "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.84.4", - "metro-cache": "0.84.4", - "metro-cache-key": "0.84.4", - "metro-config": "0.84.4", - "metro-core": "0.84.4", - "metro-file-map": "0.84.4", - "metro-resolver": "0.84.4", - "metro-runtime": "0.84.4", - "metro-source-map": "0.84.4", - "metro-symbolicate": "0.84.4", - "metro-transform-plugins": "0.84.4", - "metro-transform-worker": "0.84.4", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-config": "0.84.5", + "metro-core": "0.84.5", + "metro-file-map": "0.84.5", + "metro-resolver": "0.84.5", + "metro-runtime": "0.84.5", + "metro-source-map": "0.84.5", + "metro-symbolicate": "0.84.5", + "metro-transform-plugins": "0.84.5", + "metro-transform-worker": "0.84.5", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", @@ -34537,15 +34453,15 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.4.tgz", - "integrity": "sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.5.tgz", + "integrity": "sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", - "metro-cache-key": "0.84.4", + "metro-cache-key": "0.84.5", "nullthrows": "^1.1.1" }, "engines": { @@ -34568,24 +34484,24 @@ } }, "node_modules/metro-cache": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.4.tgz", - "integrity": "sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.5.tgz", + "integrity": "sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==", "license": "MIT", "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", - "metro-core": "0.84.4" + "metro-core": "0.84.5" }, "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-cache-key": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.4.tgz", - "integrity": "sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.5.tgz", + "integrity": "sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -34617,18 +34533,18 @@ } }, "node_modules/metro-config": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.4.tgz", - "integrity": "sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.5.tgz", + "integrity": "sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==", "license": "MIT", "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", - "metro": "0.84.4", - "metro-cache": "0.84.4", - "metro-core": "0.84.4", - "metro-runtime": "0.84.4", + "metro": "0.84.5", + "metro-cache": "0.84.5", + "metro-core": "0.84.5", + "metro-runtime": "0.84.5", "yaml": "^2.6.1" }, "engines": { @@ -34636,23 +34552,23 @@ } }, "node_modules/metro-core": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.4.tgz", - "integrity": "sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.5.tgz", + "integrity": "sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.84.4" + "metro-resolver": "0.84.5" }, "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-file-map": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.4.tgz", - "integrity": "sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.5.tgz", + "integrity": "sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -34709,9 +34625,9 @@ } }, "node_modules/metro-minify-terser": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.4.tgz", - "integrity": "sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.5.tgz", + "integrity": "sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -34722,9 +34638,9 @@ } }, "node_modules/metro-resolver": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.4.tgz", - "integrity": "sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.5.tgz", + "integrity": "sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -34734,9 +34650,9 @@ } }, "node_modules/metro-runtime": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.4.tgz", - "integrity": "sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.5.tgz", + "integrity": "sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -34747,18 +34663,18 @@ } }, "node_modules/metro-source-map": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.4.tgz", - "integrity": "sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.5.tgz", + "integrity": "sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.84.4", + "metro-symbolicate": "0.84.5", "nullthrows": "^1.1.1", - "ob1": "0.84.4", + "ob1": "0.84.5", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -34776,14 +34692,14 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.4.tgz", - "integrity": "sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.5.tgz", + "integrity": "sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.84.4", + "metro-source-map": "0.84.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" @@ -34805,9 +34721,9 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.4.tgz", - "integrity": "sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.5.tgz", + "integrity": "sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -34822,9 +34738,9 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.4.tgz", - "integrity": "sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.5.tgz", + "integrity": "sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -34832,13 +34748,13 @@ "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", - "metro": "0.84.4", - "metro-babel-transformer": "0.84.4", - "metro-cache": "0.84.4", - "metro-cache-key": "0.84.4", - "metro-minify-terser": "0.84.4", - "metro-source-map": "0.84.4", - "metro-transform-plugins": "0.84.4", + "metro": "0.84.5", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-minify-terser": "0.84.5", + "metro-source-map": "0.84.5", + "metro-transform-plugins": "0.84.5", "nullthrows": "^1.1.1" }, "engines": { @@ -35026,7 +34942,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "devOptional": true, "license": "ISC", "dependencies": { @@ -35173,9 +35091,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "funding": [ { "type": "github", @@ -35434,9 +35352,9 @@ "license": "MIT" }, "node_modules/ob1": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.4.tgz", - "integrity": "sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.5.tgz", + "integrity": "sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -36287,6 +36205,22 @@ "node": ">=4" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "devOptional": true, @@ -36405,7 +36339,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -36676,9 +36612,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "funding": [ { "type": "opencollective", @@ -36695,9 +36631,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -36842,22 +36778,21 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.3", + "version": "7.6.6", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -36982,13 +36917,6 @@ "dev": true, "license": "MIT" }, - "node_modules/queue": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "devOptional": true, @@ -37486,14 +37414,12 @@ } }, "node_modules/react-native-gesture-handler": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz", - "integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-3.3.0.tgz", + "integrity": "sha512-ZCTlJ09nE3GPbm7J74lvwpxmx1TQ/7MDlHZNLARVYTN82k21mJUJ7BmI9eWjEi8eevHiAudI5O1Cpnt0rJfoJA==", "license": "MIT", "dependencies": { - "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", - "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { @@ -37700,9 +37626,9 @@ } }, "node_modules/react-native-nitro-sqlite": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.6.0.tgz", - "integrity": "sha512-a/N1yGhM8RvCCnaYhEHhh35YS+HDOAcGKeKFsp2ExCzIjP8vPXuzQtHylgLQLeAh7rUaism5q0QQFfogXm1SXA==", + "version": "9.8.3", + "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.8.3.tgz", + "integrity": "sha512-pw4ZNYNQiMiyJZr6bu0dg+IPTskoEngg1wZBD/H5bg1oyH1rJM4HfS5nVgU7jydibIMko1YhrHK+3m5AEndcDg==", "license": "MIT", "dependencies": { "typeorm": "0.3.27" @@ -37879,9 +37805,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.116", - "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#b548d5b6b8d304444948765c389725e7ac937fa1", - "integrity": "sha512-VI0/dE6TPjt5ezG5P7k3VqKUX+U8QNaArx505n+vl0pm+m77TiU1BsNt7ikeh6Lju7GPURUXk3iAoBG+sziBUw==", + "version": "3.0.119", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.119.tgz", + "integrity": "sha512-1idaVW/nP0jTSjjevAzzMlk6gOLqkmT6gJaDYI0fWx21vtQLJ0gZeMEJb16g08Bd9nY4bsALWb4bkzhmiLWIfw==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -37890,7 +37816,8 @@ "lodash.clone": "^4.5.0", "lodash.pick": "^4.4.0", "lodash.transform": "^4.6.0", - "underscore": "^1.13.6" + "underscore": "^1.13.6", + "use-sync-external-store": "^1.6.0" }, "engines": { "node": ">=20.19.5", @@ -38189,9 +38116,9 @@ "license": "MIT" }, "node_modules/react-native-safe-area-context": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", - "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.9.1.tgz", + "integrity": "sha512-Zpx9Iwg6VhgNarWFpcIM61xK2lUbSfSXemQUmcvOVRpux49lJsUompY1S3E5jKUJbLq233qEpj28DgmXVsgZMQ==", "license": "MIT", "peerDependencies": { "react": "*", @@ -38199,9 +38126,9 @@ } }, "node_modules/react-native-screens": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.25.0.tgz", - "integrity": "sha512-CoE6W0perui0W4WK9fZFJfikUql/AYQFSJjnOGoXcPeteFb5Tursfmkot3vPOSu9lKWQMO6tlCIBQTC1CgbVRw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.28.0.tgz", + "integrity": "sha512-0/79Z8RCibuaAHti+DJ2Dq0m6UFWh4+p1bH58K3cjiQ7IDWs5kU9tCAj4lXcJMHHonbF2mIE2BeaPSEvOpIiFg==", "license": "MIT", "dependencies": { "react-freeze": "^1.0.0", @@ -38209,7 +38136,7 @@ }, "peerDependencies": { "react": "*", - "react-native": ">=0.82.0" + "react-native": "*" } }, "node_modules/react-native-share": { @@ -39769,9 +39696,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -39871,14 +39798,16 @@ "license": "ISC" }, "node_modules/simple-git": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", - "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", "dev": true, "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", "debug": "^4.4.0" }, "funding": { @@ -40147,9 +40076,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -40298,18 +40227,6 @@ "node": ">=14" } }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/stack-generator": { "version": "2.0.10", "license": "MIT", @@ -41413,9 +41330,20 @@ } }, "node_modules/strnum": { - "version": "1.0.5", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", + "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", "devOptional": true, - "license": "MIT" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } }, "node_modules/structured-headers": { "version": "0.4.1", @@ -42447,13 +42375,15 @@ } }, "node_modules/underscore": { - "version": "1.13.6", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, "node_modules/undici": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz", - "integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", "license": "MIT", "engines": { "node": ">=18.17" @@ -42691,7 +42621,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -43017,7 +42949,7 @@ } }, "node_modules/websocket-driver": { - "version": "0.7.4", + "version": "0.7.5", "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", @@ -43192,24 +43124,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, @@ -43777,19 +43691,10 @@ } }, "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, "bin": { "xlsx": "bin/xlsx.njs" }, @@ -43797,6 +43702,22 @@ "node": ">=0.8" } }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xml2js": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", diff --git a/package.json b/package.json index bdcb6ee9d114..3b0a681039ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.4.92-0", + "version": "9.5.5-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -97,7 +97,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.336", + "@expensify/react-native-live-markdown": "0.1.342", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", @@ -138,12 +138,13 @@ "array.prototype.tosorted": "^1.1.4", "awesome-phonenumber": "^5.4.0", "canvas-size": "^1.2.6", + "canvaskit-wasm": "0.41.0", "d3-scale": "^4.0.2", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.201", + "expensify-common": "2.0.207", "expo": "57.0.8", "expo-asset": "57.0.7", "expo-audio": "57.0.3", @@ -165,11 +166,11 @@ "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", "json5": "2.2.2", - "lodash-es": "4.17.21", + "lodash-es": "4.18.1", "lottie-react-native": "7.3.8", "mapbox-gl": "^3.24.0", - "metro": "0.84.4", - "metro-transform-plugins": "0.84.4", + "metro": "0.84.5", + "metro-transform-plugins": "0.84.5", "onfido-sdk-ui": "14.53.1", "pako": "^2.1.0", "process": "^0.11.10", @@ -191,7 +192,7 @@ "react-native-device-info": "10.3.1", "react-native-draggable-flatlist": "^4.0.3", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "2.32.0", + "react-native-gesture-handler": "3.3.0", "react-native-google-places-autocomplete": "2.6.4", "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.1.2", @@ -201,8 +202,8 @@ "react-native-localize": "^3.5.4", "react-native-nitro-fetch": "1.6.2", "react-native-nitro-modules": "0.37.1", - "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#b548d5b6b8d304444948765c389725e7ac937fa1", + "react-native-nitro-sqlite": "9.8.3", + "react-native-onyx": "3.0.119", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -213,8 +214,8 @@ "react-native-quick-crypto": "^1.1.6", "react-native-reanimated": "4.5.5", "react-native-render-html": "6.3.1", - "react-native-safe-area-context": "5.6.2", - "react-native-screens": "4.25.0", + "react-native-safe-area-context": "5.9.1", + "react-native-screens": "4.28.0", "react-native-share": "11.0.2", "react-native-svg": "15.15.5", "react-native-tab-view": "^4.3.0", @@ -229,7 +230,7 @@ "react-webcam": "^7.1.1", "scheduler": "0.27.0", "victory-native": "^41.21.0", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { "@aaroon/workbox-rspack-plugin": "^1.0.1", @@ -389,10 +390,11 @@ "json5": "2.2.2", "loader-utils": "2.0.4", "follow-redirects": "1.15.6", - "fast-xml-parser": "4.4.1", + "fast-xml-parser": "5.11.1", "express": "4.20.0", "elliptic": "6.5.7", "fast-json-patch": "3.1.1", + "image-size": "2.0.4", "hermes-compiler": "250829098.0.14", "webpack": "^5.108.4", "esbuild": "^0.28.1", @@ -401,6 +403,7 @@ "path-to-regexp": "0.1.10", "send": "0.19.0", "regexpu-core": "6.4.0", + "adm-zip": "0.6.1", "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "react": "19.2.3", "react-dom": "19.2.3", @@ -421,9 +424,6 @@ "@react-native-firebase/app": { "expo": "57.0.8" }, - "@react-native-firebase/perf": { - "expo": "57.0.8" - }, "@react-native-google-signin/google-signin": { "expo": "57.0.8" }, @@ -453,6 +453,10 @@ } } }, + "nitroSQLite": { + "threadSafe": true, + "performanceMode": true + }, "engines": { "bun": "1.3.14", "node": "26.5.0",