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 +1 -1
- package/src/analyze.js +3 -2
- package/src/plan.js +23 -7
- package/src/report-html.js +11 -2
- package/src/report-sarif.js +6 -1
- package/src/rules.js +15 -0
package/package.json
CHANGED
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
|
|
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,
|
|
405
|
-
//
|
|
406
|
-
const major = d.version ?
|
|
407
|
-
const same = major !== null && best &&
|
|
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
|
-
|
|
413
|
-
|
|
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;
|
package/src/report-html.js
CHANGED
|
@@ -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>
|
package/src/report-sarif.js
CHANGED
|
@@ -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) =>
|