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 +2 -2
- package/package.json +1 -1
- package/src/analyze.js +1 -1
- package/src/expo-pins.js +1 -0
- package/src/native.js +10 -6
- package/src/plan.js +7 -3
- package/src/rules.js +4 -3
- package/src/stability.js +27 -4
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.
|
|
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.
|
|
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}
|
|
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:
|
|
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 ? ', …' : ''}.
|
|
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
|
-
|
|
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
|
-
?
|
|
264
|
-
:
|
|
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
|
-
//
|
|
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',
|
|
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
|
-
|
|
312
|
-
|
|
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',
|