nativekeel 0.1.19 → 0.1.21

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.19:** better plans for apps that are far behind: library targets that fit the React Native you have, not the latest. **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. 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.21:** corrected 16 KB page dates: Google Play blocks updates without them from 1 February 2027 (high until then). **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). 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.19",
3
+ "version": "0.1.21",
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/native.js CHANGED
@@ -55,15 +55,19 @@ export function nativeChecks(project, { rnLatest = null, now = new Date() } = {}
55
55
  const hasAndroid = fs.existsSync(path.join(root, 'android'));
56
56
 
57
57
  // 16 KB page size: React Native itself before 0.77, then the actual built binaries.
58
+ const blocked16k = today >= PAGE_SIZE_16K.blockedFrom;
59
+ const policy16k = blocked16k
60
+ ? `Google Play blocks updates without 16 KB page support since ${PAGE_SIZE_16K.blockedFrom}.`
61
+ : `Google Play requires 16 KB page support for apps targeting Android 15+ (since ${PAGE_SIZE_16K.since}). Play Console warns now, and from ${PAGE_SIZE_16K.blockedFrom} it blocks updates without it.`;
58
62
  if (hasAndroid || project.managed) {
59
63
  if (rn !== null && rn < PAGE_SIZE_16K.firstRnMinor && today >= PAGE_SIZE_16K.since) {
60
64
  add({
61
65
  id: 'android-16kb-rn',
62
- severity: 'critical',
66
+ severity: blocked16k ? 'critical' : 'high',
63
67
  area: 'store',
64
68
  title: `React Native ${project.rnVersion} does not support 16 KB memory pages`,
65
- detail: `Google Play rejects updates without 16 KB page support since ${PAGE_SIZE_16K.since} (extensions ended ${PAGE_SIZE_16K.extensionUntil}). React Native supports it from 0.${PAGE_SIZE_16K.firstRnMinor}${project.expoVersion ? ', which in Expo means SDK 53 or later (SDK 52 ships React Native 0.76)' : ''}.`,
66
- fix: { kind: 'upgrade-rn-min', to: `0.${PAGE_SIZE_16K.firstRnMinor}.0` },
69
+ detail: `${policy16k} React Native supports it from 0.${PAGE_SIZE_16K.firstRnMinor}${project.expoVersion ? ', which in Expo means SDK 53 or later (SDK 52 ships React Native 0.76)' : ''}.`,
70
+ fix: { kind: 'upgrade-rn-min', to: `0.${PAGE_SIZE_16K.firstRnMinor}.0`, blocked: blocked16k, blockedFrom: PAGE_SIZE_16K.blockedFrom },
67
71
  });
68
72
  }
69
73
  const artifact = findBuiltArtifact(root);
@@ -82,11 +86,11 @@ export function nativeChecks(project, { rnLatest = null, now = new Date() } = {}
82
86
  if (relevant.length) {
83
87
  add({
84
88
  id: 'android-16kb-libs',
85
- severity: isRelease ? 'critical' : 'high',
89
+ severity: isRelease && blocked16k ? 'critical' : 'high',
86
90
  area: 'store',
87
91
  title: `${libs.length} native librar${libs.length === 1 ? 'y is' : 'ies are'} not 16 KB aligned (${rel})`,
88
- detail: `${libs.slice(0, 6).join(', ')}${libs.length > 6 ? ', …' : ''}. Google Play rejects updates containing them. Update the package that ships each library, or rebuild it with NDK r28+.`,
89
- fix: { kind: 'align-16kb', libs },
92
+ detail: `${libs.slice(0, 6).join(', ')}${libs.length > 6 ? ', …' : ''}. ${policy16k} Update the package that ships each library, or rebuild it with NDK r28+.`,
93
+ fix: { kind: 'align-16kb', libs, blocked: blocked16k, blockedFrom: PAGE_SIZE_16K.blockedFrom },
90
94
  });
91
95
  } else {
92
96
  const ignored = result.misaligned.length;
package/src/plan.js CHANGED
@@ -258,10 +258,13 @@ export function buildPlan(result) {
258
258
  } else if (privacy) {
259
259
  storeSteps.push('Add `PrivacyInfo.xcprivacy` to the iOS app target. Start from the file in the React Native template and declare the required-reason APIs your app and SDKs use (UserDefaults, file timestamps, system boot time, disk space).');
260
260
  }
261
- if (byKind('upgrade-rn-min').length) {
261
+ const min16k = byKind('upgrade-rn-min')[0];
262
+ const when16k = min16k && min16k.blocked ? 'Google Play rejects updates until' : `From ${min16k ? min16k.blockedFrom : ''} Google Play rejects updates until`;
263
+ const by16k = min16k && min16k.blocked ? 'before your next Play release' : `before ${min16k ? min16k.blockedFrom : ''}`;
264
+ if (min16k) {
262
265
  storeSteps.push(isExpo
263
- ? '**16 KB memory pages:** Google Play rejects updates until the app is on Expo SDK 53 (React Native 0.79) or later. The Expo SDK phase below gets you there; reach SDK 53 before your next Play release.'
264
- : '**16 KB memory pages:** Google Play rejects updates until the app is on React Native 0.77 or later. The React Native phase below gets you there; reach 0.77 before your next Play release.');
266
+ ? `**16 KB memory pages:** ${when16k} the app is on Expo SDK 53 (React Native 0.79) or later. The Expo SDK phase below gets you there; reach SDK 53 ${by16k}.`
267
+ : `**16 KB memory pages:** ${when16k} the app is on React Native 0.77 or later. The React Native phase below gets you there; reach 0.77 ${by16k}.`);
265
268
  }
266
269
  const align = byKind('align-16kb')[0];
267
270
  if (align) {
@@ -315,6 +318,7 @@ export function buildPlan(result) {
315
318
  for (const p of byKind('patch-version')) {
316
319
  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.`);
317
320
  }
321
+ for (const d of byKind('deprecated-core')) cleanup.push(d.step);
318
322
  if (byKind('podfile-cli-require').length) {
319
323
  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.");
320
324
  }
package/src/rules.js CHANGED
@@ -26,10 +26,11 @@ export const PLAY_TARGET_SDK = {
26
26
  visibility: { minimum: 35, since: '2026-08-31' },
27
27
  };
28
28
 
29
- // Google Play 16 KB memory page size requirement for apps targeting Android 15+.
30
- // https://android-developers.googleblog.com/2025/05/prepare-play-apps-for-devices-with-16kb-page-size.html
29
+ // Google Play 16 KB memory page size requirement for apps targeting Android 15+: policy since
30
+ // 2025-11-01; Play blocks updates without it from 2027-02-01 (checked 2026-10-06 on
31
+ // https://developer.android.com/guide/practices/page-sizes, page updated 2026-09-16).
31
32
  // React Native supports 16 KB pages from 0.77.
32
- export const PAGE_SIZE_16K = { since: '2025-11-01', extensionUntil: '2026-05-31', firstRnMinor: 77 };
33
+ export const PAGE_SIZE_16K = { since: '2025-11-01', blockedFrom: '2027-02-01', firstRnMinor: 77 };
33
34
 
34
35
  // Minimum OS versions of current React Native (since 0.76).
35
36
  // https://reactnative.dev/blog/2024/10/23/release-0.76-new-architecture
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',