nativekeel 0.1.6 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nativekeel",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Health check for React Native apps: version support, New Architecture blockers, unmaintained native modules and leaked secrets.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/analyze.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  RECENT_RELEASE_DAYS,
16
16
  STALE_RELEASE_DAYS,
17
17
  PLAY_TARGET_SDK,
18
+ RENAMED,
18
19
  REPLACEMENTS,
19
20
  SUPPORTED_EXPO_MAJORS,
20
21
  SUPPORTED_RN_MINORS,
@@ -263,7 +264,7 @@ export async function analyze(project, { get, now = new Date(), offline: forcedO
263
264
  }
264
265
  // Only code that has to follow React Native or the OS goes stale; a finished JS utility does not.
265
266
  const stale = !dep.unmaintained && !revived && age !== null && age > STALE_RELEASE_DAYS && (native || !!release.rnLink);
266
- const alternatives = dir && dir.alternatives && dir.alternatives.length ? dir.alternatives : REPLACEMENTS[name] || [];
267
+ const alternatives = RENAMED[name] ? [RENAMED[name]] : dir && dir.alternatives && dir.alternatives.length ? dir.alternatives : REPLACEMENTS[name] || [];
267
268
  if (noNewArch || dep.unmaintained || stale) {
268
269
  const problems = [
269
270
  noNewArch && 'no New Architecture support',
@@ -279,7 +280,7 @@ export async function analyze(project, { get, now = new Date(), offline: forcedO
279
280
  area: 'dependency',
280
281
  title: `${name}: ${problems.join(', ')}`,
281
282
  detail: `${why}${alternatives.length ? ` Alternatives: ${alternatives.join(', ')}.` : ''}`,
282
- fix: { kind: 'replace-dep', name, native, noNewArch, unmaintained: dep.unmaintained || stale, alternatives },
283
+ fix: { kind: 'replace-dep', name, native, noNewArch, unmaintained: dep.unmaintained || stale, alternatives, renamedTo: RENAMED[name] || null },
283
284
  });
284
285
  } else if (revived) {
285
286
  add({
package/src/plan.js CHANGED
@@ -314,6 +314,9 @@ export function buildPlan(result) {
314
314
  why: 'These packages decide how far you can upgrade. Swap them while the app still runs on its current version, so every problem has one cause.',
315
315
  steps: replace.map((d) => {
316
316
  const reason = [d.noNewArch && 'no New Architecture support', d.unmaintained && 'unmaintained'].filter(Boolean).join(', ');
317
+ if (d.renamedTo) {
318
+ return `\`${d.name}\` moved to \`${d.renamedTo}\` (same API). \`${pm.remove} ${d.name} && ${pm.add} ${d.renamedTo}\`, then update the imports.`;
319
+ }
317
320
  if (interopPatched.has(d.name) && !d.unmaintained) {
318
321
  return `\`${d.name}\` (${reason}, native). The New Architecture step below patches it so it loads; move to a release with a TurboModule spec once one exists.`;
319
322
  }
@@ -397,20 +400,33 @@ export function buildPlan(result) {
397
400
  let from = rn.from;
398
401
  const tracked = result.deps.filter((d) => COMPAT[d.name]);
399
402
  const lastRange = {};
400
- const steps = upgradeHops(rn.from, rn.to).map((to) => {
403
+ const lastMajor = {};
404
+ const hopList = upgradeHops(rn.from, rn.to);
405
+ // When the New Architecture switch happens on the way, library versions up to that stop
406
+ // must still run on the legacy architecture (Reanimated 4, for one, does not).
407
+ const switchAt = deferredArch
408
+ ? hopList.findIndex((h) => Number(h.split('.')[1]) >= 76 && Number(h.split('.')[1]) < NEW_ARCH_ONLY_MINOR)
409
+ : -1;
410
+ const majorOf = (v) => Number(String(v).split('.')[0]);
411
+ const steps = hopList.map((to, i) => {
401
412
  const minor = Number(to.split('.')[1]);
413
+ const archOn = switchAt < 0 || i > switchAt;
402
414
  const bumps = tracked
403
415
  .map((d) => {
404
- const best = bestRange(d.name, minor, true);
405
- // Prefer staying on the installed major when it still supports this hop.
406
- const major = d.version ? Number(d.version.split('.')[0]) : null;
407
- const same = major !== null && best && Number(best.from.split('.')[0]) !== major ? bestRange(d.name, minor, true, { major }) : null;
416
+ const best = bestRange(d.name, minor, archOn);
417
+ // Stay on the major of the previous step (or the installed one) while it still works.
418
+ const major = lastMajor[d.name] ?? (d.version ? majorOf(d.version) : null);
419
+ const same = major !== null && best && majorOf(best.from) !== major ? bestRange(d.name, minor, archOn, { major }) : null;
408
420
  return { name: d.name, best, same };
409
421
  })
410
422
  .filter((b) => b.best && (b.same || b.best).range !== lastRange[b.name])
411
423
  .map((b) => {
412
- lastRange[b.name] = (b.same || b.best).range;
413
- return b.same ? `\`${b.name}\` ${b.same.range} (or ${b.best.range}, a major migration)` : `\`${b.name}\` ${b.best.range}`;
424
+ const chosen = b.same || b.best;
425
+ const forcedMajor = !b.same && lastMajor[b.name] !== undefined && majorOf(chosen.from) !== lastMajor[b.name];
426
+ lastRange[b.name] = chosen.range;
427
+ lastMajor[b.name] = majorOf(chosen.from);
428
+ if (b.same) return `\`${b.name}\` ${b.same.range} (or ${b.best.range}, a major migration)`;
429
+ return `\`${b.name}\` ${b.best.range}${forcedMajor ? ' (a major migration: read its migration guide first)' : ''}`;
414
430
  });
415
431
  const step = `${from} → ${to}: apply the diff from ${UPGRADE_HELPER_URL(from, to)}${bumps.length ? `, move ${bumps.join(' and ')}` : ''}, reinstall pods, build both platforms, commit.`;
416
432
  from = to;
@@ -15,11 +15,19 @@ export function htmlReport(result, { brand = null, plan = null } = {}) {
15
15
  const defaultOn = !!result.project.reactNative && Number(result.project.reactNative.split('.')[1]) >= 76;
16
16
  const arch = (v) => (v === true ? 'on' : v === false ? 'off' : defaultOn ? 'on (default)' : 'off (default)');
17
17
 
18
+ // Outdated packages are many and alike: one collapsible table instead of a card each.
19
+ const isBump = (f) => f.id.startsWith('dep-major:');
18
20
  const rows = SEVERITIES.flatMap((sev) =>
19
21
  result.findings
20
- .filter((f) => f.severity === sev)
22
+ .filter((f) => f.severity === sev && !isBump(f))
21
23
  .map((f) => `<li class="f ${sev}"><span class="tag">${sevLabel[sev]}</span><div><strong>${rich(f.title)}</strong><p>${rich(f.detail)}</p></div></li>`),
22
24
  ).join('\n');
25
+ const bumps = result.findings.filter(isBump);
26
+ const bumpTable = bumps.length
27
+ ? `<details class="bumps"><summary>${bumps.length} package${bumps.length === 1 ? ' is' : 's are'} a major version behind <span class="muted">(low)</span></summary><table><thead><tr><th>Package</th><th>Installed</th><th>Latest</th><th></th></tr></thead><tbody>${bumps
28
+ .map((f) => `<tr><td>${esc(f.fix.name)}</td><td>${esc(f.fix.from || '')}</td><td>${esc(f.fix.to || '')}</td><td class="muted">${f.fix.native ? 'native' : ''}</td></tr>`)
29
+ .join('')}</tbody></table></details>`
30
+ : '';
23
31
 
24
32
  const planHtml = plan
25
33
  ? `<section><h2>Upgrade plan <small>scope: ${esc(plan.complexity.size)}</small></h2>${plan.phases
@@ -50,6 +58,7 @@ h1{font-size:26px;margin:0 0 4px}h2{font-size:19px;margin:32px 0 12px}h2 small{f
50
58
  .stat.critical b{color:var(--critical)}.stat.high b{color:var(--high)}
51
59
  .facts{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 16px;margin-top:12px;display:flex;flex-wrap:wrap;gap:6px 24px;font-size:14px}
52
60
  ul.list{list-style:none;padding:0;margin:0;display:grid;gap:8px}
61
+ details.bumps{margin-top:8px;background:var(--card);border:1px solid var(--line);border-left:4px solid var(--low);border-radius:8px;padding:10px 14px}details.bumps summary{cursor:pointer;font-weight:600}details.bumps table{width:100%;border-collapse:collapse;margin-top:10px;font-size:14px}details.bumps th,details.bumps td{text-align:left;padding:4px 8px;border-top:1px solid var(--line)}.muted{color:var(--muted);font-weight:400}
53
62
  .f{display:flex;gap:12px;background:var(--card);border:1px solid var(--line);border-left:4px solid var(--low);border-radius:8px;padding:10px 14px}
54
63
  .f p{margin:2px 0 0;color:var(--muted);font-size:14px}.f strong{overflow-wrap:anywhere}
55
64
  .tag{flex:none;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.03em;min-width:64px;padding-top:2px}
@@ -80,7 +89,7 @@ ${p.android && p.android.targetSdk ? `<span>targetSdk <b>${p.android.targetSdk}<
80
89
  <span>${result.deps.length} dependencies (${result.deps.filter((d) => d.native).length} native)</span>
81
90
  </div>
82
91
  <h2>Findings</h2>
83
- ${result.findings.length ? `<ul class="list">${rows}</ul>` : '<p>No issues found.</p>'}
92
+ ${result.findings.length ? `${rows ? `<ul class="list">${rows}</ul>` : ''}${bumpTable}` : '<p>No issues found.</p>'}
84
93
  ${planHtml}
85
94
  <footer>${footer}</footer>
86
95
  </main></body></html>
@@ -49,7 +49,7 @@ export function sarifReport(result, { version = '0.0.0' } = {}) {
49
49
  ruleId: f.id.split(':')[0],
50
50
  level: LEVEL[f.severity],
51
51
  message: { text: `${f.title}. ${f.detail}` },
52
- locations: [{ physicalLocation: { artifactLocation: { uri: loc.uri }, region: { startLine: loc.line } } }],
52
+ locations: [{ physicalLocation: { artifactLocation: { uri: toUri(loc.uri) }, region: { startLine: loc.line } } }],
53
53
  // Stable across runs so code scanning tracks one alert per finding.
54
54
  partialFingerprints: { nativekeelId: f.id },
55
55
  properties: { severity: f.severity, area: f.area },
@@ -59,3 +59,8 @@ export function sarifReport(result, { version = '0.0.0' } = {}) {
59
59
  ],
60
60
  };
61
61
  }
62
+
63
+ // SARIF artifact locations are URIs: encode each path segment (spaces, #, %), keep the slashes.
64
+ function toUri(file) {
65
+ return file.split(/[\\/]/).map(encodeURIComponent).join('/');
66
+ }
package/src/rules.js CHANGED
@@ -70,6 +70,21 @@ export const REPLACEMENTS = {
70
70
  'react-native-print': ['expo-print'],
71
71
  'react-native-i18n': ['react-native-localize', 'expo-localization'],
72
72
  'expo-av': ['expo-audio', 'expo-video'],
73
+ 'react-native-snap-carousel': ['react-native-reanimated-carousel'],
74
+ 'react-native-iphone-x-helper': ['react-native-safe-area-context'],
75
+ 'react-native-slider': ['@react-native-community/slider'],
76
+ 'react-native-datepicker': ['@react-native-community/datetimepicker'],
77
+ };
78
+
79
+ // Packages that moved to a new name with the same API: swap the package, update the imports.
80
+ export const RENAMED = {
81
+ '@react-native-community/async-storage': '@react-native-async-storage/async-storage',
82
+ '@react-native-community/cameraroll': '@react-native-camera-roll/camera-roll',
83
+ '@react-native-community/masked-view': '@react-native-masked-view/masked-view',
84
+ '@react-native-community/viewpager': 'react-native-pager-view',
85
+ '@react-native-community/picker': '@react-native-picker/picker',
86
+ '@react-native-community/clipboard': '@react-native-clipboard/clipboard',
87
+ 'react-native-netinfo': '@react-native-community/netinfo',
73
88
  };
74
89
 
75
90
  export const UPGRADE_HELPER_URL = (from, to) =>