Skip to content

♻️ refactor(expo): route all platform-sensitive deps through lib/ wrappers#2564

Open
andrew-bierman wants to merge 6 commits into
developmentfrom
refactor/lib-wrapped-imports
Open

♻️ refactor(expo): route all platform-sensitive deps through lib/ wrappers#2564
andrew-bierman wants to merge 6 commits into
developmentfrom
refactor/lib-wrapped-imports

Conversation

@andrew-bierman

@andrew-bierman andrew-bierman commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

What

Extends the apps/expo/lib/ wrapper convention to every platform-sensitive native dependency, so call sites import one cross-platform path instead of the raw module — no Platform.OS branches, and expo export -p web keeps working.

Establishes the rule introduced in #2366 (which wrapped expo-secure-store, expo-apple-authentication, expo-updates) and applies it consistently across the app.

New wrappers

Module Wrapper Web behavior
@react-native-async-storage/async-storage lib/asyncStorage.ts re-export (package has a built-in localStorage web impl)
expo-sqlite/kv-store lib/expoSqliteKvStore.ts (+.web.ts) localStorage-backed shim mirroring the sync + async API
@react-native-google-signin/google-signin lib/googleSignin.ts (+.web.ts) stub that fails loudly only if a web caller triggers native sign-in

Enforcement

scripts/lint/no-direct-wrapped-imports.ts upgraded from a 3-entry map to a structured WrappedEntry[] covering all 6 wrapped modules, each with its own exempt wrapper files. A direct import of any wrapped module anywhere in apps/expo (outside the wrapper + tests) now fails lint:custom. This is what makes the convention self-sustaining: "if something goes into lib/, everyone uses lib/."

scripts/lint/no-owned-max-params.ts exempts the two web shims that must mirror their native module's positional (key, value) API.

Call sites routed through wrappers (16)

atomWith{Async,Kv}Storage, kvStorage(.web), persist-plugin(.web), utils/storage, wildlifeAtoms, chatStorageAtoms, useAuthActions, useAuthInit, settings/index, auth/index, profile/index, useTrailConditionReports, LocationSearchScreen.

Risk

Near-zero. On native, every wrapper re-exports the identical module, so runtime behavior is unchanged. The web shims are additive. Verified:

  • bun run lint:custom (all 8 checks, incl. no-direct-wrapped-imports)
  • bunx expo export -p web builds clean
  • ✅ lefthook biome pre-commit clean

Minor note for reviewers

expoSqliteKvStore.web.ts's clear() calls localStorage.clear() (broader than native kv-store's isolated-table clear). It is currently unused — all .clear() call sites go through AsyncStorage, whose web behavior is untouched by this PR — so it's a theoretical-only sharp edge. Flagging in case a future web caller wires it up.

Context

Pairs with the planned @packrat/app package extraction — a single wrapped import surface per dependency makes that extraction mechanical rather than a platform-branch untangle.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Improved persistence reliability by routing storage access through unified Expo-backed wrappers.
    • Updated authentication and related storage flows to use the same internal abstractions for more consistent behavior across platforms.
    • Added web-compatible implementations for storage and Google Sign-In placeholders, improving cross-platform consistency.
  • Chores
    • Enhanced lint enforcement to prevent direct imports of platform-sensitive dependencies outside the approved wrapper modules.

Add apps/expo/lib/ wrapper modules so platform-sensitive native/expo
dependencies are imported through a single seam, with .web.ts variants
where web behavior diverges:

- secureStore (+ .web localStorage shim — expo-secure-store throws on web)
- googleSignin (+ .web stub — native-only module)
- expoSqliteKvStore (+ .web localStorage shim — native SQLite has no web build)
- asyncStorage (single re-export — works on web via localStorage)

Add scripts/lint/no-direct-wrapped-imports.ts with a WRAPPED map that
fails when any wrapped module is imported directly outside its wrapper,
and wire it into lint:custom.
Replace direct imports of expo-secure-store, expo-apple-authentication,
expo-updates, @react-native-async-storage/async-storage,
@react-native-google-signin/google-signin, and expo-sqlite/kv-store with
their apps/expo/lib/ wrappers across 19 files (call sites, jotai storage
atoms, the API/auth clients, and the existing kvStorage/persist-plugin
web wrappers). Behavior-preserving on native; the .web.ts variants now
back web auth and storage so no Platform branches are needed.
…rapped-imports

# Conflicts:
#	apps/expo/app/(app)/(tabs)/profile/index.tsx
#	apps/expo/features/ai/atoms/chatStorageAtoms.ts
#	apps/expo/features/auth/hooks/useAuthActions.ts
#	apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts
#	apps/expo/features/weather/screens/LocationSearchScreen.tsx
#	apps/expo/lib/auth-client.ts
#	apps/expo/lib/secureStore.ts
#	apps/expo/lib/secureStore.web.ts
#	scripts/lint/no-direct-wrapped-imports.ts
The web shim mirrors expo-sqlite/kv-store's positional (key, value) API
(setItem/setItemSync/setItemAsync), same as the secureStore.web shim.
Copilot AI review requested due to automatic review settings June 5, 2026 18:17
@github-actions github-actions Bot added the mobile label Jun 5, 2026
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR abstracts Expo app storage and authentication dependencies by creating local wrapper modules (expo-app/lib/*). Base wrappers re-export AsyncStorage and SQLite KV storage; web-specific shims provide localStorage and Google Sign-In stubs. Consumer modules (atoms, hooks, screens) and storage adapters are updated to import from wrappers instead of third-party packages. Lint rules enforce the new pattern to prevent regression.

Changes

Storage and Authentication Wrapper Abstraction

Layer / File(s) Summary
Base storage wrapper modules and web shims
apps/expo/lib/asyncStorage.ts, apps/expo/lib/expoSqliteKvStore.ts, apps/expo/lib/expoSqliteKvStore.web.ts
asyncStorage.ts re-exports React Native AsyncStorage; expoSqliteKvStore.ts re-exports the SQLite KV singleton; .web.ts implements localStorage-backed sync and async equivalents (getItemSync, setItemSync, removeItemSync, getAllKeysSync, plus wrapped async versions).
Google Sign-In wrapper modules and web fallback
apps/expo/lib/googleSignin.ts, apps/expo/lib/googleSignin.web.ts
googleSignin.ts forwards named exports from the native package; .web.ts provides placeholder stubs where signIn/signInSilently throw via notAvailable(), other methods return inert values, type-guard helpers always return false, and GoogleSigninButton renders null.
Storage adapter modules
apps/expo/lib/kvStorage.ts, apps/expo/lib/kvStorage.web.ts, apps/expo/lib/persist-plugin.ts, apps/expo/lib/persist-plugin.web.ts
kvStorage wraps sync-style KV access using Storage from the base wrappers; persist-plugin.ts wires storage to observablePersistSqlite; .web.ts uses AsyncStorage for web observablePersistAsyncStorage.
Atom storage consumers
apps/expo/atoms/atomWithAsyncStorage.ts, apps/expo/atoms/atomWithKvStorage.ts, apps/expo/features/ai/atoms/chatStorageAtoms.ts, apps/expo/features/wildlife/atoms/wildlifeAtoms.ts
Atom modules updated to import storage from wrapper modules; atom mounting (load from storage or use initial value) and write behavior (persist JSON) remain unchanged.
Hook consumers
apps/expo/features/auth/hooks/useAuthActions.ts, apps/expo/features/auth/hooks/useAuthInit.ts, apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts
Auth hooks updated to import AsyncStorage, GoogleSignin (and helpers), and Storage from wrapper modules; hook logic and exports unchanged.
Screen, feature, and utility consumers
apps/expo/app/(app)/(tabs)/profile/index.tsx, apps/expo/app/(app)/settings/index.tsx, apps/expo/app/auth/index.tsx, apps/expo/features/weather/screens/LocationSearchScreen.tsx, apps/expo/utils/storage.ts
Screens and utilities updated to import AsyncStorage from wrapper module; all call sites (AsyncStorage.setItem, getItem, clear, etc.) remain functionally the same.
Lint enforcement rules
scripts/lint/no-direct-wrapped-imports.ts, scripts/lint/no-owned-max-params.ts
no-direct-wrapped-imports.ts refactored to enforce apps/expo-scoped wrapped-module checking: regex-based import detection, wrapper exemption via WrappedEntry config, stricter file filtering (excluding test/spec). no-owned-max-params.ts adds expoSqliteKvStore.web.ts to exclusions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PackRat-AI/PackRat#2364: Initial web shims and persistence setup that established the foundation for storage module abstraction now being enforced across consumers in this PR.

Suggested labels

web, mobile, dependencies

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: refactoring platform-sensitive dependencies to route through centralized lib/ wrapper modules.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/lib-wrapped-imports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/mcp (./packages/mcp)

Status Category Percentage Covered / Total
🔵 Lines 98.87% (🎯 95%) 176 / 178
🔵 Statements 98.87% (🎯 95%) 176 / 178
🔵 Functions 100% (🎯 95%) 13 / 13
🔵 Branches 98.38% (🎯 90%) 61 / 62
File CoverageNo changed files found.
Generated in workflow #295 for commit c9b064b by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/analytics (./packages/analytics)

Status Category Percentage Covered / Total
🔵 Lines 93.68% (🎯 80%) 697 / 744
🔵 Statements 93.68% (🎯 80%) 697 / 744
🔵 Functions 97.87% (🎯 85%) 46 / 47
🔵 Branches 85.8% (🎯 80%) 133 / 155
File CoverageNo changed files found.
Generated in workflow #295 for commit c9b064b by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/units (./packages/units)

Status Category Percentage Covered / Total
🔵 Lines 100% (🎯 100%) 35 / 35
🔵 Statements 100% (🎯 100%) 35 / 35
🔵 Functions 100% (🎯 100%) 6 / 6
🔵 Branches 100% (🎯 100%) 11 / 11
File CoverageNo changed files found.
Generated in workflow #295 for commit c9b064b by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/overpass (./packages/overpass)

Status Category Percentage Covered / Total
🔵 Lines 100% (🎯 80%) 155 / 155
🔵 Statements 100% (🎯 80%) 155 / 155
🔵 Functions 100% (🎯 80%) 13 / 13
🔵 Branches 95.65% (🎯 70%) 44 / 46
File CoverageNo changed files found.
Generated in workflow #295 for commit c9b064b by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for apps/expo (./apps/expo)

Status Category Percentage Covered / Total
🔵 Lines 97.52% (🎯 95%) 590 / 605
🔵 Statements 97.52% (🎯 95%) 590 / 605
🔵 Functions 100% (🎯 97%) 51 / 51
🔵 Branches 95.3% (🎯 92%) 203 / 213
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/expo/utils/storage.ts 100% 100% 100% 100%
Generated in workflow #295 for commit c9b064b by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/api (./packages/api)

Status Category Percentage Covered / Total
🔵 Lines 98.95% (🎯 95%) 1322 / 1336
🔵 Statements 98.95% (🎯 95%) 1322 / 1336
🔵 Functions 100% (🎯 97%) 74 / 74
🔵 Branches 95.62% (🎯 92%) 481 / 503
File CoverageNo changed files found.
Generated in workflow #295 for commit c9b064b by the Vitest Coverage Report Action

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
packrat-admin c9b064b Commit Preview URL

Branch Preview URL
Jun 16 2026, 04:07 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Deploying packrat-guides with  Cloudflare Pages  Cloudflare Pages

Latest commit: c9b064b
Status: ✅  Deploy successful!
Preview URL: https://b1f50038.packrat-guides-6gq.pages.dev
Branch Preview URL: https://refactor-lib-wrapped-imports.packrat-guides-6gq.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/expo/features/auth/hooks/useAuthActions.ts (1)

88-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard data before dereferencing data.user in sign-in.

Line 88 checks error only, but Line 90 assumes data exists. A { error: null, data: null } response would throw before your intended auth error flow.

Suggested fix
-      if (error) throw toAuthError({ source: error, fallback: 'Sign in failed' });
-      applySession(data.user as Record<string, unknown>); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime
+      if (error || !data?.user) {
+        throw toAuthError({ source: error, fallback: 'Sign in failed' });
+      }
+      applySession(data.user as Record<string, unknown>); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime

As per coding guidelines, "API client responses must be checked with if (error || !data) before using the data object."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/auth/hooks/useAuthActions.ts` around lines 88 - 90, Guard
the sign-in response before dereferencing data.user: in the code path using
authClient.signIn.email(...) check for both error and missing data (if (error ||
!data) ...) and throw toAuthError({ source: error, fallback: 'Sign in failed' })
when either occurs; only call applySession(data.user as Record<string, unknown>)
after this guard so data is guaranteed to exist. Use the existing toAuthError
and applySession symbols and keep the error message/fallback consistent with
current behavior.
apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts (1)

60-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Capture the original API error, not a wrapped new Error, in Sentry.

Line 62 creates a new error and Line 63 captures that wrapper, which drops original error context.

Suggested fix
   if (error) {
     console.error('Failed to fetch trail condition reports:', error.value);
-    const err = new Error(`Failed to fetch trail condition reports: ${String(error.value)}`);
-    Sentry.captureException(err, {
+    Sentry.captureException(error, {
       tags: { feature: 'trailConditions', action: 'fetchReports' },
       extra: { trailName, apiError: error.value, httpStatus: error.status },
     });
-    throw err;
+    throw new Error(`Failed to fetch trail condition reports: ${String(error.value)}`);
   }

As per coding guidelines, "Never wrap the original error in new Error(...) before passing to Sentry's captureException."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts` around
lines 60 - 67, The code currently creates a wrapper Error (err) and passes that
to Sentry, losing the original API error context; change Sentry.captureException
to send the original error object (error.value) instead of err, while still
supplying the same tags/extra (use error.status, trailName, apiError:
error.value). Keep the console.error and continue to throw the existing err if
you need a normalized thrown Error, but ensure
Sentry.captureException(error.value, { tags: {...}, extra: {...} }) so the
original error is preserved; update the Sentry call at the
Sentry.captureException invocation in useTrailConditionReports.ts accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/expo/lib/expoSqliteKvStore.web.ts`:
- Around line 41-43: The clear() implementation currently calls
localStorage.clear(), wiping all keys; change it to only remove keys managed by
this KV store by using a unique prefix (or a stored registry) for this Storage
instance and iterating through localStorage keys to remove only those that start
with that prefix; update clear (and any removeItem/getItem/getItemSync wrappers
like clear:(), getItemSync, setItem/removeItem) to add/remove/lookup keys with
that prefix so no unrelated localStorage entries are deleted, and ensure any
existing getItemSync calls prepend the same prefix when reading.
- Around line 13-15: The web shim setItemSync currently only accepts a string
but must mirror the kv-store contract that allows an UpdateFn; update
setItemSync (and similarly setItemAsync/setItem shims if present) to accept
value: string | UpdateFn, detect when value is a function, call getItemSync(key)
to obtain the current string (treat null/undefined as null), invoke the UpdateFn
with that current value to compute the new string, and then call
localStorage.setItem(key, finalValue); ensure the function signature/overload
reflects UpdateFn and that non-function values are stored unchanged.

In `@scripts/lint/no-direct-wrapped-imports.ts`:
- Around line 78-80: The isTargetFile function currently only treats TS
extensions as targets and only strips tests/specs via a suffix regex that
ignores JS-family files; update isTargetFile to include JS/JSX/MJS/CJS
extensions (js, jsx, mjs, cjs) in the positive extension check and in the
test/spec exclusion check so filenames like *.test.js or *.spec.mjs are
excluded; locate the isTargetFile function and broaden both regexes to cover
(ts|tsx|cts|mts|js|jsx|mjs|cjs) and ensure the test/spec pattern matches before
any of those extensions.

---

Outside diff comments:
In `@apps/expo/features/auth/hooks/useAuthActions.ts`:
- Around line 88-90: Guard the sign-in response before dereferencing data.user:
in the code path using authClient.signIn.email(...) check for both error and
missing data (if (error || !data) ...) and throw toAuthError({ source: error,
fallback: 'Sign in failed' }) when either occurs; only call
applySession(data.user as Record<string, unknown>) after this guard so data is
guaranteed to exist. Use the existing toAuthError and applySession symbols and
keep the error message/fallback consistent with current behavior.

In `@apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts`:
- Around line 60-67: The code currently creates a wrapper Error (err) and passes
that to Sentry, losing the original API error context; change
Sentry.captureException to send the original error object (error.value) instead
of err, while still supplying the same tags/extra (use error.status, trailName,
apiError: error.value). Keep the console.error and continue to throw the
existing err if you need a normalized thrown Error, but ensure
Sentry.captureException(error.value, { tags: {...}, extra: {...} }) so the
original error is preserved; update the Sentry call at the
Sentry.captureException invocation in useTrailConditionReports.ts accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 20370310-5843-404a-91b6-3a72f9f2775d

📥 Commits

Reviewing files that changed from the base of the PR and between dbebb1a and 625d67f.

📒 Files selected for processing (23)
  • apps/expo/app/(app)/(tabs)/profile/index.tsx
  • apps/expo/app/(app)/settings/index.tsx
  • apps/expo/app/auth/index.tsx
  • apps/expo/atoms/atomWithAsyncStorage.ts
  • apps/expo/atoms/atomWithKvStorage.ts
  • apps/expo/features/ai/atoms/chatStorageAtoms.ts
  • apps/expo/features/auth/hooks/useAuthActions.ts
  • apps/expo/features/auth/hooks/useAuthInit.ts
  • apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts
  • apps/expo/features/weather/screens/LocationSearchScreen.tsx
  • apps/expo/features/wildlife/atoms/wildlifeAtoms.ts
  • apps/expo/lib/asyncStorage.ts
  • apps/expo/lib/expoSqliteKvStore.ts
  • apps/expo/lib/expoSqliteKvStore.web.ts
  • apps/expo/lib/googleSignin.ts
  • apps/expo/lib/googleSignin.web.ts
  • apps/expo/lib/kvStorage.ts
  • apps/expo/lib/kvStorage.web.ts
  • apps/expo/lib/persist-plugin.ts
  • apps/expo/lib/persist-plugin.web.ts
  • apps/expo/utils/storage.ts
  • scripts/lint/no-direct-wrapped-imports.ts
  • scripts/lint/no-owned-max-params.ts

Comment on lines +13 to +15
const setItemSync = (key: string, value: string): void => {
localStorage.setItem(key, value);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for setItemSync calls with function arguments.

# Test: Look for setItemSync calls passing callbacks or arrow functions.
rg -nP --type=ts --type=tsx 'setItemSync\s*\(\s*[^,]+,\s*(\(|function)' apps/expo/

Repository: PackRat-AI/PackRat

Length of output: 90


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the target web shim around the reported lines
echo "---- File head (apps/expo/lib/expoSqliteKvStore.web.ts) ----"
nl -ba apps/expo/lib/expoSqliteKvStore.web.ts | sed -n '1,120p'

# 2) Find the declaration(s) of setItemSync
echo
echo "---- setItemSync declarations ----"
rg -n --hidden --no-ignore -S "setItemSync\s*=" apps/expo

# 3) Search for any calls that pass a function as the second argument
echo
echo "---- setItemSync calls passing function/arrow as 2nd arg ----"
rg -n --hidden --no-ignore -S "setItemSync\s*\(\s*[^,]+,\s*(\(|function|async\s*\()" apps/expo -g'*.ts' -g'*.tsx'

# 4) Also search for setItemSync calls generally (to see how it's used)
echo
echo "---- setItemSync call sites (general) ----"
rg -n --hidden --no-ignore -S "setItemSync\s*\(" apps/expo -g'*.ts' -g'*.tsx' | head -n 200

Repository: PackRat-AI/PackRat

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show file with line numbers (without nl)
echo "---- File head (apps/expo/lib/expoSqliteKvStore.web.ts) ----"
awk '{printf "%d\t%s\n", NR, $0}' apps/expo/lib/expoSqliteKvStore.web.ts | sed -n '1,160p'

echo
echo "---- setItemSync declarations ----"
rg -n --hidden --no-ignore -S "setItemSync\s*=" apps/expo -g'*.ts' -g'*.tsx'

echo
echo "---- setItemSync calls passing function/arrow as 2nd arg ----"
rg -n --hidden --no-ignore -S 'setItemSync\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' apps/expo -g'*.ts' -g'*.tsx'

echo
echo "---- setItemSync call sites (general) ----"
rg -n --hidden --no-ignore -S 'setItemSync\s*\(' apps/expo -g'*.ts' -g'*.tsx' | head -n 200

Repository: PackRat-AI/PackRat

Length of output: 2279


🏁 Script executed:

#!/bin/bash
set -u
file="apps/expo/lib/expoSqliteKvStore.web.ts"

echo "---- $file (relevant snippet) ----"
awk 'NR>=1 && NR<=120 {printf "%d\t%s\n", NR, $0}' "$file"

echo
echo "---- setItemSync calls passing function/arrow as 2nd arg (should be empty) ----"
rg -n --hidden --no-ignore -S 'setItemSync\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' apps/expo -g'*.ts' -g'*.tsx' || true

echo
echo "---- setItemSync call sites (2nd arg types) ----"
rg -n --hidden --no-ignore -S 'setItemSync\s*\(' apps/expo -g'*.ts' -g'*.tsx' || true

echo
echo "---- Storage.setItemSync call sites ----"
rg -n --hidden --no-ignore -S 'Storage\.(setItemSync)\s*\(' apps/expo -g'*.ts' -g'*.tsx' || true

echo
echo "---- Any UpdateFn-like usage for setItemSync ----"
rg -n --hidden --no-ignore -S '(UpdateFn|prev\s*=>|prev\s*\))' apps/expo -g'*.ts' -g'*.tsx' || true

Repository: PackRat-AI/PackRat

Length of output: 6353


🏁 Script executed:

#!/bin/bash
set -u

echo "---- apps/expo/lib/kvStorage.ts ----"
awk 'NR>=1 && NR<=200 {printf "%d\t%s\n", NR, $0}' apps/expo/lib/kvStorage.ts

echo
echo "---- apps/expo/mocks/expo-sqlite-kv-store.ts (UpdateFn + setItemSync/setItem patterns) ----"
awk 'NR>=1 && NR<=220 {printf "%d\t%s\n", NR, $0}' apps/expo/mocks/expo-sqlite-kv-store.ts

echo
echo "---- Call sites: setItemSync with function/arrow as 2nd arg (should be empty) ----"
rg -n --hidden --no-ignore -S 'setItemSync\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' apps/expo -g'*.ts' -g'*.tsx' || true

echo
echo "---- Call sites: Storage.setItemSync with function/arrow as 2nd arg ----"
rg -n --hidden --no-ignore -S 'Storage\.setItemSync\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' apps/expo -g'*.ts' -g'*.tsx' || true

echo
echo "---- Call sites: setItem* with function/arrow as 2nd arg (any) ----"
rg -n --hidden --no-ignore -S 'setItem(?:Async|Sync)?\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' apps/expo -g'*.ts' -g'*.tsx' || true

echo
echo "---- Call sites: Storage.setItem* with function/arrow as 2nd arg ----"
rg -n --hidden --no-ignore -S 'Storage\.setItem(?:Async|Sync)?\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' apps/expo -g'*.ts' -g'*.tsx' || true

echo
echo "---- Call sites: setItem* where 2nd arg is an arrow function specifically (prev => ...) ----"
rg -n --hidden --no-ignore -S 'setItem(?:Async|Sync)?\s*\(\s*[^,]+,\s*[^,=]*=>\s*' apps/expo -g'*.ts' -g'*.tsx' || true

Repository: PackRat-AI/PackRat

Length of output: 5905


🏁 Script executed:

#!/bin/bash
set -u

echo "---- Find UpdateFn definitions/usages in apps/expo/lib ----"
rg -n --hidden --no-ignore -S "UpdateFn" apps/expo/lib

echo
echo "---- Inspect expoSqliteKvStore type surface (files named expoSqliteKvStore.*) ----"
fd -a -t f "expoSqliteKvStore" apps/expo/lib apps/expo -E node_modules

echo
echo "---- Print expoSqliteKvStore files (small regions around setItem signatures) ----"
for f in $(fd -a -t f "expoSqliteKvStore" apps/expo/lib apps/expo -E node_modules); do
  echo "===== $f (setItem signatures) ====="
  rg -n --hidden --no-ignore -S "setItem(Async|Sync)?\s*\(|setItem\s*:\s*|UpdateFn|prevValue" "$f" || true
  echo "===== $f (around setItemSync) ====="
  rg -n --hidden --no-ignore -S "setItemSync" "$f" | head -n 20 || true
done

Repository: PackRat-AI/PackRat

Length of output: 2007


🏁 Script executed:

#!/bin/bash
set -u

file="apps/expo/lib/expoSqliteKvStore.ts"

echo "---- $file (first 220 lines) ----"
awk 'NR<=220 {printf "%d\t%s\n", NR, $0}' "$file" | head -n 220

echo
echo "---- $file: lines containing setItem/setItemSync/UpdateFn ----"
rg -n --hidden --no-ignore -S "setItemSync|setItemAsync|setItem\s*\(|UpdateFn|prevValue|UpdateFn|value:\s*string" "$file"

Repository: PackRat-AI/PackRat

Length of output: 626


🏁 Script executed:

#!/bin/bash
set -u
echo "---- Locate expo-sqlite/kv-store type files ----"
rg -n --hidden --no-ignore -S "expo-sqlite/kv-store" package.json apps/expo -g'package.json' -g'*.ts' -g'*.tsx' || true

echo
echo "---- Search node_modules for UpdateFn and setItem signatures ----"
rg -n --hidden --no-ignore -S "type UpdateFn|interface.*UpdateFn|UpdateFn\s*=\s*\(|UpdateFn" node_modules -g'*.ts' -g'*.d.ts' -g'*.tsx' 2>/dev/null | head -n 50 || true

echo
rg -n --hidden --no-ignore -S "setItemSync\s*\(" node_modules -g'*.d.ts' -g'*.ts' 2>/dev/null | head -n 50 || true

echo
rg -n --hidden --no-ignore -S "setItem\s*\(" node_modules -g'*.d.ts' -g'*.ts' 2>/dev/null | head -n 50 || true

echo
echo "---- If present, print the kv-store .d.ts around setItem/setItemSync ----"
kvpath=$(rg -l --hidden --no-ignore -S "setItemSync\s*\(" node_modules -g'*.d.ts' 2>/dev/null | rg -m 1 "kv-store" || true)
echo "kvpath=$kvpath"
if [ -n "${kvpath:-}" ]; then
  sed -n '1,200p' "$kvpath" | cat
fi

Repository: PackRat-AI/PackRat

Length of output: 581


🏁 Script executed:

#!/bin/bash
set -u

echo "---- setItemSync calls passing function/arrow as 2nd arg (repo-wide) ----"
rg -n --hidden --no-ignore -S 'setItemSync\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' . -g'*.ts' -g'*.tsx' || true

echo
echo "---- setItemAsync calls passing function/arrow as 2nd arg (repo-wide) ----"
rg -n --hidden --no-ignore -S 'setItemAsync\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' . -g'*.ts' -g'*.tsx' || true

echo
echo "---- Storage.setItemSync / Storage.setItemAsync / Storage.setItem calls passing arrow/function (repo-wide) ----"
rg -n --hidden --no-ignore -S '(Storage|AsyncStorage)\.setItem(?:Sync|Async)?\s*\(\s*[^,]+,\s*(\(|function|async\s*\()' . -g'*.ts' -g'*.tsx' || true

echo
echo "---- setItem* where 2nd arg contains an arrow '=>' (repo-wide; may include other setState calls) ----"
rg -n --hidden --no-ignore -S 'setItem(?:Sync|Async)?\s*\(\s*[^,]+,\s*\(?\s*[^)]*=>\s*' . -g'*.ts' -g'*.tsx' || true

Repository: PackRat-AI/PackRat

Length of output: 428


Align web setItemSync with UpdateFn-capable kv-store contract

No call sites pass a functional updater to setItemSync/setItemAsync/setItem in this repo, so nothing breaks today. However, apps/expo/mocks/expo-sqlite-kv-store.ts supports setItemSync(key, value: string | UpdateFn) while apps/expo/lib/expoSqliteKvStore.web.ts only accepts value: string, creating a contract mismatch—add the UpdateFn overload to the web shim and derive the final value from getItemSync(key) when value is a function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/lib/expoSqliteKvStore.web.ts` around lines 13 - 15, The web shim
setItemSync currently only accepts a string but must mirror the kv-store
contract that allows an UpdateFn; update setItemSync (and similarly
setItemAsync/setItem shims if present) to accept value: string | UpdateFn,
detect when value is a function, call getItemSync(key) to obtain the current
string (treat null/undefined as null), invoke the UpdateFn with that current
value to compute the new string, and then call localStorage.setItem(key,
finalValue); ensure the function signature/overload reflects UpdateFn and that
non-function values are stored unchanged.

Comment on lines +41 to +43
clear: (): Promise<void> => {
localStorage.clear();
return Promise.resolve();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: clear() destroys all localStorage data, not just KV store scope.

Native clearSync() only removes keys in the isolated SQLite table. This web shim calls localStorage.clear(), which wipes everything in localStorage—breaking unrelated features that persist data there.

🛡️ Proposed fix to scope clear() to managed keys

Track a prefix or registry of managed keys. For now, iterate all keys and remove only those set via this Storage instance:

+const STORAGE_PREFIX = '__expo_kv_';
+
 const setItemSync = (key: string, value: string): void => {
-  localStorage.setItem(key, value);
+  localStorage.setItem(STORAGE_PREFIX + key, value);
 };

 const removeItemSync = (key: string): void => {
-  localStorage.removeItem(key);
+  localStorage.removeItem(STORAGE_PREFIX + key);
 };

 const getAllKeysSync = (): string[] => {
   const keys: string[] = [];
   for (let i = 0; i < localStorage.length; i++) {
     const key = localStorage.key(i);
-    if (key !== null) keys.push(key);
+    if (key !== null && key.startsWith(STORAGE_PREFIX)) {
+      keys.push(key.slice(STORAGE_PREFIX.length));
+    }
   }
   return keys;
 };

 // ... in Storage object:
   clear: (): Promise<void> => {
-    localStorage.clear();
+    const keys = getAllKeysSync();
+    keys.forEach(k => removeItemSync(k));
     return Promise.resolve();
   },

Adjust getItemSync similarly to prepend prefix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/lib/expoSqliteKvStore.web.ts` around lines 41 - 43, The clear()
implementation currently calls localStorage.clear(), wiping all keys; change it
to only remove keys managed by this KV store by using a unique prefix (or a
stored registry) for this Storage instance and iterating through localStorage
keys to remove only those that start with that prefix; update clear (and any
removeItem/getItem/getItemSync wrappers like clear:(), getItemSync,
setItem/removeItem) to add/remove/lookup keys with that prefix so no unrelated
localStorage entries are deleted, and ensure any existing getItemSync calls
prepend the same prefix when reading.

Comment on lines +78 to 80
function isTargetFile(name: string): boolean {
return /\.(ts|tsx|cts|mts)$/.test(name) && !/\.(test|spec)\.(ts|tsx|cts|mts)$/.test(name);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether JS-family files exist under apps/expo and can currently bypass this lint.
fd -t f -e js -e jsx -e mjs -e cjs . apps/expo \
  | rg -n -v '(app\.config\.js|metro\.config\.js|\.test\.|\.spec\.)'

# Check for direct wrapped imports in apps/expo across JS/TS files.
rg -nP "(?:from|import)\s+['\"](expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)(?:/[^'\"]*)?['\"]" apps/expo \
  -g '!**/*.test.*' -g '!**/*.spec.*'

Repository: PackRat-AI/PackRat

Length of output: 577


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the lint script around isTargetFile + call site to confirm what gets passed.
echo "=== scripts/lint/no-direct-wrapped-imports.ts (relevant region) ==="
sed -n '60,160p' scripts/lint/no-direct-wrapped-imports.ts

# 2) JS-only search: files with .js/.jsx/.mjs/.cjs importing the flagged packages.
echo
echo "=== JS-family files under apps/expo that import/export flagged modules ==="
rg -nP --glob '**/*.{js,jsx,mjs,cjs}' \
  "(?:from|import)\s+['\"](expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)(?:/[^'\"]*)?['\"]" \
  apps/expo \
  --glob '!**/*.test.*' \
  --glob '!**/*.spec.*'

# 3) Path-based test dir check (e.g., __tests__/...) for flagged imports in any JS/TS.
echo
echo "=== Flagged imports inside __tests__/ under apps/expo (any language) ==="
rg -nP \
  "(?:from|import)\s+['\"](expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)(?:/[^'\"]*)?['\"]" \
  apps/expo \
  --glob '**/__tests__/**'

Repository: PackRat-AI/PackRat

Length of output: 3198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Helper: don't fail on no matches
rg_no_fail() {
  # usage: rg_no_fail <rg_args...>
  rg "$@" || true
}

echo "=== JS-family files under apps/expo (for context) ==="
fd -t f . apps/expo -e js -e jsx -e mjs -e cjs | wc -l

echo
echo "=== Flagged-module imports in JS-family files under apps/expo (no fail on no matches) ==="
rg_no_fail -nP \
  --glob '**/*.{js,jsx,mjs,cjs}' \
  "(?:from|import)\s+['\"](expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)(?:/[^'\"]*)?['\"]" \
  apps/expo \
  --glob '!**/*.test.*' \
  --glob '!**/*.spec.*'

echo
echo "=== Flagged-module imports inside __tests__/ under apps/expo (no fail on no matches) ==="
rg_no_fail -nP \
  "(?:from|import)\s+['\"](expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)(?:/[^'\"]*)?['\"]" \
  apps/expo \
  --glob '**/__tests__/**'

Repository: PackRat-AI/PackRat

Length of output: 414


🏁 Script executed:

#!/bin/bash
set -euo pipefail

MOD_PATTERN='(expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)'

# Find all files in apps/expo that directly import the wrapped modules
echo "=== Files with direct imports of wrapped modules under apps/expo ==="
rg -lP "(?:from|import)\s+['\"]${MOD_PATTERN}(?:/[^'\"]*)?['\"]" apps/expo || true

# Evaluate whether lint would treat each matched file as a "target" based on current isTargetFile logic
# isTargetFile(name): ts/tsx/cts/mts AND NOT .(test|spec).(ts|tsx|cts|mts)
echo
echo "=== Lint inclusion check (simulating isTargetFile on basename) ==="
python3 - <<'PY'
import os, re, subprocess, shlex, sys

mod_pattern = '(expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)'
cmd = ["rg","-lP",f"(?:from|import)\\s+['\"]{mod_pattern}(?:/[^'\"]*)?['\"]","apps/expo"]
files = subprocess.check_output(cmd, text=True).splitlines()
if not files:
    print("(no matches)")
    sys.exit(0)

def is_target(name: str) -> bool:
    return bool(re.search(r"\.(ts|tsx|cts|mts)$", name)) and not bool(re.search(r"\.(test|spec)\.(ts|tsx|cts|mts)$", name))

for p in sorted(files):
    base = os.path.basename(p)
    print(f"{p} -> isTargetFile(basename={base!r})={is_target(base)}")
PY

# Also specifically check for direct imports in JS-family files (to validate the bypass)
echo
echo "=== JS-family files with direct imports of wrapped modules (if any) ==="
rg -nP "(?:from|import)\s+['\"]${MOD_PATTERN}(?:/[^'\"]*)?['\"]" apps/expo \
  --glob '**/*.{js,jsx,mjs,cjs}' --glob '!**/*.test.*' --glob '!**/*.spec.*' || true

Repository: PackRat-AI/PackRat

Length of output: 1078


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== scripts/lint/no-direct-wrapped-imports.ts (top: EXPO_ROOT + WRAPPED) ==="
sed -n '1,120p' scripts/lint/no-direct-wrapped-imports.ts

python3 - <<'PY'
import re, subprocess, os, pathlib, json

path = 'scripts/lint/no-direct-wrapped-imports.ts'
txt = pathlib.Path(path).read_text('utf-8')

# Extract wrapperFiles string literals like 'lib/googleSignin.ts'
wrapper_files = set(re.findall(r"wrapperFiles:\s*\[\s*([^\]]+)\s*\]", txt, flags=re.S))
# The above regex grabs whole arrays; do a simpler pass for 'lib/...'
wrapper_file_literals = set(re.findall(r"'lib/[^']+'", txt))
wrapper_file_literals_web = set(re.findall(r"'lib/[^']+\.web\.ts'", txt))
wrapper_file_set = set(x.strip("'") for x in wrapper_file_literals)

# Build import regex for direct imports of wrapped modules
modules = [
  'expo-secure-store',
  'expo-apple-authentication',
  'expo-updates',
  '`@react-native-async-storage/async-storage`',
  '`@react-native-google-signin/google-signin`',
  'expo-sqlite/kv-store',
]
mod_pat = '(' + '|'.join(map(re.escape, modules)) + ')'

rg_cmd = ["rg","-lP",f"(?:from|import)\\s+['\"]{mod_pat}(?:/[^'\"]*)?['\"]","apps/expo"]
files = subprocess.check_output(rg_cmd, text=True).splitlines()
print("\n=== Direct-import files found (apps/expo) ===")
print("\n".join(files) if files else "(no matches)")

def is_target(basename: str) -> bool:
  return bool(re.search(r"\.(ts|tsx|cts|mts)$", basename)) and not bool(re.search(r"\.(test|spec)\.(ts|tsx|cts|mts)$", basename))

def rel_entry(p: str) -> str:
  # lint uses entryRel relative to EXPO_ROOT (apps/expo), which is the path without the prefix
  return p.removeprefix('apps/expo/')

# Determine which direct-import files would actually be considered target and non-wrapper by the lint
violating_candidates = []
for f in files:
  base = os.path.basename(f)
  entry_rel = rel_entry(f)
  if not is_target(base):
    continue
  if entry_rel in wrapper_file_set:
    continue
  violating_candidates.append((f, base, entry_rel))

print("\n=== Would the current lint flag any non-wrapper direct-import files? ===")
if not violating_candidates:
  print("(no)")
else:
  for f, base, entry_rel in violating_candidates:
    print(f"- {f} (basename={base}, entryRel={entry_rel})")

# List JS-family files under apps/expo for context
fd_cmd = ["fd","-t","f",".","apps/expo","-e","js","-e","jsx","-e","mjs","-e","cjs"]
js_files = subprocess.check_output(fd_cmd, text=True).splitlines()
print("\n=== JS/JSX/MJS/CJS files under apps/expo ===")
print("\n".join(js_files) if js_files else "(none)")
PY

echo
echo "=== Direct imports restricted to JS-family files (sanity) ==="
rg -nP "(?:from|import)\s+['\"](expo-secure-store|expo-apple-authentication|expo-updates|`@react-native-async-storage/async-storage`|`@react-native-google-signin/google-signin`|expo-sqlite/kv-store)(?:/[^'\"]*)?['\"]" \
  apps/expo --glob '**/*.{js,jsx,mjs,cjs}' --glob '!**/*.test.*' --glob '!**/*.spec.*' || true

Repository: PackRat-AI/PackRat

Length of output: 4812


Loosened enforcement isn’t needed right now, but hardening isTargetFile is still worthwhile.

The current rule already finds no violations in apps/expo: direct imports of the wrapped modules only occur in the wrapper files under apps/expo/lib/* (exempted) and in apps/expo/utils/__tests__/storage.test.ts (excluded by the existing .test.ts suffix). There are no direct wrapped-module imports in apps/expo JS/JSX/MJS/CJS sources (only config files in that extension set).

Still, isTargetFile currently ignores .js/.jsx/.mjs/.cjs entirely and does suffix-based test/spec filtering only on the basename, so future .js usage or test layouts not using .test/.spec suffixes could bypass the rule. Consider the broader detection below.

🔧 Proposed fix (optional hardening)
-function isTargetFile(name: string): boolean {
-  return /\.(ts|tsx|cts|mts)$/.test(name) && !/\.(test|spec)\.(ts|tsx|cts|mts)$/.test(name);
+function isTargetFile(relPath: string): boolean {
+  const isSource = /\.(ts|tsx|cts|mts|js|jsx|cjs|mjs)$/.test(relPath);
+  const isTestLike =
+    /(?:^|\/)__tests__(?:\/|$)/.test(relPath) ||
+    /\.(test|spec)\.(ts|tsx|cts|mts|js|jsx|cjs|mjs)$/.test(relPath);
+  return isSource && !isTestLike;
 }
@@
-    if (!isTargetFile(entry)) continue;
+    if (!isTargetFile(entryRel)) continue;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lint/no-direct-wrapped-imports.ts` around lines 78 - 80, The
isTargetFile function currently only treats TS extensions as targets and only
strips tests/specs via a suffix regex that ignores JS-family files; update
isTargetFile to include JS/JSX/MJS/CJS extensions (js, jsx, mjs, cjs) in the
positive extension check and in the test/spec exclusion check so filenames like
*.test.js or *.spec.mjs are excluded; locate the isTargetFile function and
broaden both regexes to cover (ts|tsx|cts|mts|js|jsx|mjs|cjs) and ensure the
test/spec pattern matches before any of those extensions.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

The Web E2E job built the web app against a remote dev API
(secrets.EXPO_PUBLIC_API_URL) and seeded the real dev DB. The remote API
was unreachable from the runner, so Better Auth sign-in failed at
globalSetup.ts ("Failed to fetch", status 0) and the whole job went red.

Rework it to stand up the full stack locally, mirroring the Maestro job
and the documented local web-e2e setup:

- Start Postgres (pgvector) + the Neon HTTP proxy via
  packages/api/docker-compose.test.yml. The proxy lets wrangler dev's
  @neondatabase/serverless HTTP driver hit local Postgres on :4444 — the
  same driver path as prod, reliable under the web flow's query load
  (verified locally: repeated sign-ins 200, no DB timeouts).
- Migrations + seed connect to the raw Postgres port (:5433) so
  migrate.ts uses the node-postgres driver; db.localtest.me:4444 is the
  HTTP proxy, not the pg wire protocol. Both target the same database.
- Write .dev.vars with ENVIRONMENT=development so Better Auth trusts
  http://localhost:* origins (web app is served cross-origin on :8081),
  and NEON_DATABASE_URL pointed at the proxy.
- Build the web bundle against the local API (http://localhost:8787),
  serve on :8081, run Playwright against it.
- Drop the NEON_DEV_DATABASE_URL dependency from the e2e-gate; the stack
  is fully local now. Also trigger on packages/api/** changes.

Verified locally end-to-end: docker stack up, migrate + seed via raw pg,
wrangler dev healthy, and cross-origin POST /api/auth/sign-in/email
returns 200 with Access-Control-Allow-Origin + an HttpOnly
better-auth.session_token cookie.
@github-actions github-actions Bot added the ci/cd label Jun 6, 2026
@github-actions github-actions Bot removed the ci/cd label Jun 16, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown
Contributor

Deploying packrat-landing with  Cloudflare Pages  Cloudflare Pages

Latest commit: c9b064b
Status: ✅  Deploy successful!
Preview URL: https://68189c6f.packrat-landing.pages.dev
Branch Preview URL: https://refactor-lib-wrapped-imports.packrat-landing.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/expo/features/auth/hooks/useAuthActions.ts (1)

259-289: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard sign-out cleanup so the flow can’t dead-end.

Line 283 awaits clearLocalData() inside finally. If cleanup fails, signOut() rejects before the caller can present post-sign-out actions, while Line 263 has already set redirecting state to true. This can leave logout UX/state stuck on failure paths.

Proposed fix
   } finally {
     userStore.set(null);
-    await clearLocalData();
+    try {
+      await clearLocalData();
+    } catch (cleanupError) {
+      Sentry.captureException(cleanupError, {
+        tags: { auth_action: 'sign_out', phase: 'cleanup' },
+      });
+    }
     setNeedsReauth(false);
     // isLoadingAtom intentionally left true — the caller (profile/handleSignOut)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/auth/hooks/useAuthActions.ts` around lines 259 - 289, The
signOut function's finally block awaits clearLocalData() without error handling.
If clearLocalData() rejects, the entire signOut promise will reject, preventing
the caller from presenting post-sign-out actions while setIsSignOutRedirecting
has already been set to true, leaving the UX stuck. Guard the clearLocalData()
call in the finally block with a try-catch (or .catch()) to ensure any cleanup
failures are caught silently or logged without rejecting the signOut flow,
allowing the caller to proceed with post-sign-out actions regardless of cleanup
success.
apps/expo/features/auth/hooks/useAuthInit.ts (1)

154-161: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a Sentry helper call in the background refresh catch path.

The catch block at Lines 154-161 currently swallows transient failures without a breadcrumb or capture, which removes diagnostic visibility for auth-init failures.

Proposed fix
           .catch((error) => {
+            Sentry.addBreadcrumb({
+              category: 'auth',
+              message: 'Background session refresh failed',
+              level: 'warning',
+            });
             if (isDefinitiveAuthFailure(error)) {
               Sentry.setUser(null);
               userStore.set(null);
               router.replace('/auth');
             }
             // Network/transient error — keep cached user silently
           });

As per coding guidelines, “In apps/expo, import Sentry instrumentation from @sentry/react-native and call helpers before async operations and in all catch blocks.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/auth/hooks/useAuthInit.ts` around lines 154 - 161, Add
Sentry instrumentation to the catch block in useAuthInit.ts to capture
diagnostic visibility for transient auth failures. In the catch handler around
lines 154-161, after the isDefinitiveAuthFailure check, add a Sentry helper call
(such as captureException or addBreadcrumb) for the transient error case before
the comment about keeping the cached user silently. Ensure Sentry is imported
from `@sentry/react-native` at the top of the file, and call the appropriate
Sentry method to log the transient error without treating it as a definitive
auth failure.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/expo/features/auth/hooks/useAuthActions.ts`:
- Around line 259-289: The signOut function's finally block awaits
clearLocalData() without error handling. If clearLocalData() rejects, the entire
signOut promise will reject, preventing the caller from presenting post-sign-out
actions while setIsSignOutRedirecting has already been set to true, leaving the
UX stuck. Guard the clearLocalData() call in the finally block with a try-catch
(or .catch()) to ensure any cleanup failures are caught silently or logged
without rejecting the signOut flow, allowing the caller to proceed with
post-sign-out actions regardless of cleanup success.

In `@apps/expo/features/auth/hooks/useAuthInit.ts`:
- Around line 154-161: Add Sentry instrumentation to the catch block in
useAuthInit.ts to capture diagnostic visibility for transient auth failures. In
the catch handler around lines 154-161, after the isDefinitiveAuthFailure check,
add a Sentry helper call (such as captureException or addBreadcrumb) for the
transient error case before the comment about keeping the cached user silently.
Ensure Sentry is imported from `@sentry/react-native` at the top of the file, and
call the appropriate Sentry method to log the transient error without treating
it as a definitive auth failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9d827074-9414-4f51-b966-6e90c27185c9

📥 Commits

Reviewing files that changed from the base of the PR and between 625d67f and c9b064b.

📒 Files selected for processing (5)
  • apps/expo/app/(app)/(tabs)/profile/index.tsx
  • apps/expo/app/(app)/settings/index.tsx
  • apps/expo/features/auth/hooks/useAuthActions.ts
  • apps/expo/features/auth/hooks/useAuthInit.ts
  • scripts/lint/no-owned-max-params.ts
💤 Files with no reviewable changes (1)
  • scripts/lint/no-owned-max-params.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants