nativekeel 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Health check for React Native and Expo apps. One command, no install, no account
6
6
  npx nativekeel
7
7
  ```
8
8
 
9
- **What's new in 0.1.18:** the upgrade plan only suggests library versions your current React Native supports, moves React Navigation and RN Firebase packages together, and says plainly when a vulnerable package has no fixed release. **0.1.17:** every vulnerable package shows the version that fixes it; Expo apps get one line for the packages the SDK upgrade moves. Recent releases added a *Start here* list in every report, hacking risks mapped to OWASP MASVS, crash and performance checks. Every check is tuned on 450+ open-source React Native and Expo apps to keep false alarms out. [All releases →](https://nativekeel.com/changelog)
9
+ **What's new in 0.1.20:** warns about `ImageBackground`, `SafeAreaView`, `DrawerLayoutAndroid` and `UTFSequence`, which React Native has deprecated and will remove (143 of 470 apps we test import one). **0.1.19:** better plans for apps that are far behind: library targets that fit the React Native you have, not the latest. Recent releases added a *Start here* list in every report, hacking risks mapped to OWASP MASVS, crash and performance checks. Every check is tuned on 450+ open-source React Native and Expo apps to keep false alarms out. [All releases →](https://nativekeel.com/changelog)
10
10
 
11
11
  ```
12
12
  Summary 4 critical · 5 high · 4 medium · 6 low
@@ -30,7 +30,7 @@ Start here
30
30
  - **Will Google Play and the App Store accept your next build?** `targetSdk`, 16 KB memory pages (it also opens your built APK/AAB and checks every native library), the iOS privacy manifest, and missing Info.plist usage descriptions.
31
31
 
32
32
  **Crashes**
33
- - **Will it crash at runtime?** Reanimated without its Babel plugin or `react-native-worklets`, mixed `@react-native-firebase/*` or `@react-navigation/*` majors, a second copy of React or React Native inside a dependency, Kotlin modules that cannot load through the New Architecture interop layer, AppDelegate setups that crash in Release, camera/photos/location access without the Info.plist text iOS requires, imports of APIs removed from React Native core (`AsyncStorage`, `Picker`, `ViewPropTypes`, …), and traps learned from real upgrades.
33
+ - **Will it crash at runtime?** Reanimated without its Babel plugin or `react-native-worklets`, mixed `@react-native-firebase/*` or `@react-navigation/*` majors, a second copy of React or React Native inside a dependency, Kotlin modules that cannot load through the New Architecture interop layer, AppDelegate setups that crash in Release, camera/photos/location access without the Info.plist text iOS requires, imports of APIs removed from React Native core (`AsyncStorage`, `Picker`, `ViewPropTypes`, …) or deprecated for removal (`ImageBackground`, `SafeAreaView`), and traps learned from real upgrades.
34
34
 
35
35
  **Security, grouped by OWASP MASVS**
36
36
  - **Secrets in the app or the repo:** AWS, Stripe, OpenAI, Anthropic, GitHub, Slack, SendGrid, Twilio, Shopify, Google OAuth and Supabase `service_role` keys, private keys, `.env` values compiled into the bundle, Expo `extra`/`EXPO_PUBLIC_`/`eas.json` secrets, signing keys and passwords.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nativekeel",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Health check for React Native and Expo apps: upgrade blockers, store deadlines, crash risks, vulnerable dependencies and leaked secrets, with a step-by-step upgrade plan.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/analyze.js CHANGED
@@ -218,7 +218,7 @@ export async function analyze(project, { get, now = new Date(), offline: forcedO
218
218
  severity: 'critical',
219
219
  area: 'store',
220
220
  title: `targetSdk ${target}: Google Play will reject your next update`,
221
- detail: `Since ${updates.since} new apps and updates must target API ${updates.minimum}+ (extension possible until ${updates.extensionUntil}).`,
221
+ detail: `Since ${updates.since} new apps and updates must target API ${updates.minimum}+${today <= updates.extensionUntil ? ` (an extension you request in Play Console runs until ${updates.extensionUntil} at the latest)` : '; the extension period has ended'}.`,
222
222
  fix: { kind: 'target-sdk', from: target, to: updates.minimum },
223
223
  });
224
224
  }
package/src/expo-pins.js CHANGED
@@ -11,6 +11,7 @@ const EXPO_PINNED = new Set([
11
11
  '@react-native-async-storage/async-storage', '@react-native-community/datetimepicker', '@react-native-community/slider',
12
12
  '@react-native-community/netinfo', '@react-native-picker/picker', '@react-native-masked-view/masked-view',
13
13
  'lottie-react-native', '@shopify/flash-list', '@shopify/react-native-skia', '@stripe/stripe-react-native',
14
+ '@react-native-segmented-control/segmented-control', '@sentry/react-native', 'react-native-bootsplash',
14
15
  ]);
15
16
 
16
17
  export function expoPinnedNames(root) {
package/src/plan.js CHANGED
@@ -177,6 +177,8 @@ export function buildPlan(result) {
177
177
  const pm = isExpo ? { ...basePm, add: 'npx expo install' } : basePm;
178
178
  const pinned = isExpo ? expoPinnedNames(result.project.root) : new Set();
179
179
  const movesWithExpo = (name) => isExpo && movesWithExpoSdk(name, pinned);
180
+ // Packages the React Native upgrade steps move (React Native itself and the compatibility tables).
181
+ const movesWithRn = (name) => !isExpo && !!byKind('upgrade-rn').length && (name === 'react-native' || !!COMPAT[name]);
180
182
  const installed = new Set(result.deps.map((d) => d.name));
181
183
  const byKind = (kind) => result.findings.filter((f) => f.fix && f.fix.kind === kind).map((f) => f.fix);
182
184
  const phases = [];
@@ -290,7 +292,7 @@ export function buildPlan(result) {
290
292
  } else if (f.fix.kind === 'vuln-dep' && (f.fix.severity === 'critical' || f.fix.severity === 'high')) {
291
293
  const v = f.fix;
292
294
  if (v.noFix) sec.push({ sev, step: `\`${v.name}\` ${v.from} has ${v.severity} known vulnerabilities and no fixed release (the latest is ${v.latest}): replace it, or make sure it never handles untrusted input.` });
293
- else sec.push({ sev, step: `Update \`${v.name}\` ${v.from}${v.to ? ` to ${v.to}${v.to.startsWith('a release') ? '' : ' or later'}` : ''}: it has ${v.severity} known vulnerabilities. ${movesWithExpo(v.name) ? 'Expo pins it: the SDK upgrade below moves it.' : 'Check its changelog for breaking changes.'}` });
295
+ else sec.push({ sev, step: `Update \`${v.name}\` ${v.from}${v.to ? ` to ${v.to}${v.to.startsWith('a release') ? '' : ' or later'}` : ''}: it has ${v.severity} known vulnerabilities. ${movesWithExpo(v.name) ? 'Expo pins it: the SDK upgrade below moves it.' : movesWithRn(v.name) ? 'The React Native steps below move it past this version; if you ship before them, update it now.' : 'Check its changelog for breaking changes.'}` });
294
296
  }
295
297
  }
296
298
  const securitySteps = sec.sort((a, b) => a.sev - b.sev).map((x) => x.step);
@@ -313,6 +315,7 @@ export function buildPlan(result) {
313
315
  for (const p of byKind('patch-version')) {
314
316
  cleanup.push(`\`patches/${p.file}\` targets ${p.pkg} ${p.version} but ${p.installed} is installed: check whether the fix is still needed, then recreate or delete it.`);
315
317
  }
318
+ for (const d of byKind('deprecated-core')) cleanup.push(d.step);
316
319
  if (byKind('podfile-cli-require').length) {
317
320
  cleanup.push("Delete `require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'` from `ios/Podfile`; `use_native_modules!` comes from React Native.");
318
321
  }
@@ -369,17 +372,21 @@ export function buildPlan(result) {
369
372
  // The React Native steps move them further.
370
373
  const curMinor = result.project.reactNative ? Number(result.project.reactNative.split('.')[1]) : null;
371
374
  const archNow = !byKind('enable-new-arch').length;
375
+ const rnTarget = byKind('upgrade-rn')[0];
376
+ const rnBehind = !!(rnTarget && curMinor !== null && Number(rnTarget.to.split('.')[1]) - curMinor >= 3);
372
377
  const nativeBumps = byKind('bump-dep')
373
378
  .filter((d) => d.native && !movesWithExpo(d.name))
374
379
  .map((d) => {
375
380
  if (!COMPAT[d.name] || curMinor === null) return d;
376
381
  const best = bestRange(d.name, curMinor, archNow);
377
- if (!best) return d;
382
+ if (!best) return null; // the table starts later: the React Native steps move it
378
383
  if (compareVersions(d.from, best.from) >= 0 && (!best.to || compareVersions(d.from, best.to) <= 0)) return null;
379
384
  if (compareVersions(d.from, best.from) > 0) return null;
380
385
  return { ...d, to: best.range, capped: `0.${curMinor}` };
381
386
  })
382
- .filter(Boolean);
387
+ .filter(Boolean)
388
+ // Far behind: the latest release usually needs a newer React Native than the app has.
389
+ .map((d) => (rnBehind && !d.capped ? { ...d, to: `the newest release that supports React Native 0.${curMinor} (latest is ${d.to})` } : d));
383
390
  if (nativeBumps.length) {
384
391
  phases.push({
385
392
  title: 'Update native modules on the current React Native version',
package/src/stability.js CHANGED
@@ -277,10 +277,19 @@ const REMOVED_FROM_CORE = {
277
277
  ART: 'react-native-svg',
278
278
  };
279
279
  const REMOVED_PROP_TYPES = ['ViewPropTypes', 'ColorPropType', 'EdgeInsetsPropType', 'PointPropType'];
280
+ // Deprecated in core and announced for removal (RN index.js warnOnce, checked 0.78-0.87).
281
+ // They still work; moving now avoids a crash when they are removed.
282
+ const DEPRECATED_IN_CORE = {
283
+ SafeAreaView: { since: 81, use: 'react-native-safe-area-context' },
284
+ ImageBackground: { since: 87, use: 'a View with an absolutely positioned Image' },
285
+ DrawerLayoutAndroid: { since: 87, use: 'react-native-drawer-layout' },
286
+ UTFSequence: { since: 87, use: "Unicode escapes written directly (e.g. '\\ufeff')" },
287
+ };
280
288
 
281
289
  export function removedCoreImports(project) {
282
290
  const root = project.root;
283
291
  const used = new Map(); // name -> first file:line
292
+ const deprecated = new Map();
284
293
  const files = [];
285
294
  const walk = (dir) => {
286
295
  let entries = [];
@@ -308,17 +317,31 @@ export function removedCoreImports(project) {
308
317
  for (const b of blocks) {
309
318
  for (const raw of b.names.split(',')) {
310
319
  const name = raw.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0].trim();
311
- if ((REMOVED_FROM_CORE[name] || REMOVED_PROP_TYPES.includes(name)) && !used.has(name)) {
312
- used.set(name, `${path.relative(root, file)}:${text.slice(0, b.index).split('\n').length}`);
313
- }
320
+ const at = () => `${path.relative(root, file)}:${text.slice(0, b.index).split('\n').length}`;
321
+ if ((REMOVED_FROM_CORE[name] || REMOVED_PROP_TYPES.includes(name)) && !used.has(name)) used.set(name, at());
322
+ if (DEPRECATED_IN_CORE[name] && !deprecated.has(name)) deprecated.set(name, at());
314
323
  }
315
324
  }
316
325
  }
317
- if (!used.size) return [];
318
326
  const minor = project.rnVersion ? Number(project.rnVersion.split('.')[1]) : null;
327
+ const findings = [];
328
+ if (deprecated.size) {
329
+ const names = [...deprecated.keys()];
330
+ const warnsNow = minor !== null && names.some((n) => minor >= DEPRECATED_IN_CORE[n].since);
331
+ findings.push({
332
+ id: 'deprecated-core-imports',
333
+ severity: 'low',
334
+ area: 'react-native',
335
+ title: `${names.join(', ')} ${names.length === 1 ? 'is' : 'are'} deprecated in React Native core and will be removed`,
336
+ detail: `${[...deprecated].map(([n, at]) => `${n} (${at}, deprecated in 0.${DEPRECATED_IN_CORE[n].since}) → ${DEPRECATED_IN_CORE[n].use}`).join('; ')}. ${warnsNow ? 'They already log a deprecation warning.' : 'They start logging a warning when you upgrade.'} Once removed, the import returns undefined and the screen crashes, so move off them while it is a small change.`,
337
+ fix: { kind: 'deprecated-core', step: `Replace ${names.map((n) => `\`${n}\` (→ ${DEPRECATED_IN_CORE[n].use})`).join(', ')} before React Native removes ${names.length === 1 ? 'it' : 'them'}.` },
338
+ });
339
+ }
340
+ if (!used.size) return findings;
319
341
  const crashesNow = [...used.keys()].some((n) => (REMOVED_PROP_TYPES.includes(n) ? minor >= 74 : minor >= 72));
320
342
  const list = [...used].map(([n, at]) => `${n} (${at}) → ${REMOVED_FROM_CORE[n] || 'deprecated-react-native-prop-types'}`);
321
343
  return [
344
+ ...findings,
322
345
  {
323
346
  id: 'crash-removed-core-imports',
324
347
  severity: crashesNow ? 'high' : 'medium',