nativekeel 0.1.0

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.
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import zlib from 'node:zlib';
4
+
5
+ // Checks native libraries inside a built APK/AAB for 16 KB page alignment, the same thing
6
+ // Google's check_elf_alignment.sh does: every PT_LOAD segment of every 64-bit .so must be
7
+ // aligned to at least 16 KB. Pure Node: a minimal ZIP reader plus an ELF header parser.
8
+
9
+ const PAGE_16K = 16384;
10
+ const ABIS_64 = ['arm64-v8a', 'x86_64'];
11
+
12
+ function readZipEntries(buf) {
13
+ // End of central directory record is in the last 64 KB (+22 bytes).
14
+ const min = Math.max(0, buf.length - 65557);
15
+ let eocd = -1;
16
+ for (let i = buf.length - 22; i >= min; i--) {
17
+ if (buf.readUInt32LE(i) === 0x06054b50) {
18
+ eocd = i;
19
+ break;
20
+ }
21
+ }
22
+ if (eocd < 0) throw new Error('not a zip file');
23
+ let count = buf.readUInt16LE(eocd + 10);
24
+ let cdOffset = buf.readUInt32LE(eocd + 16);
25
+
26
+ // ZIP64: large APKs/AABs may need the ZIP64 end of central directory.
27
+ if (cdOffset === 0xffffffff || count === 0xffff) {
28
+ const locator = eocd - 20;
29
+ if (locator >= 0 && buf.readUInt32LE(locator) === 0x07064b50) {
30
+ const z64 = Number(buf.readBigUInt64LE(locator + 8));
31
+ count = Number(buf.readBigUInt64LE(z64 + 32));
32
+ cdOffset = Number(buf.readBigUInt64LE(z64 + 48));
33
+ }
34
+ }
35
+
36
+ const entries = [];
37
+ let p = cdOffset;
38
+ for (let i = 0; i < count; i++) {
39
+ if (buf.readUInt32LE(p) !== 0x02014b50) break;
40
+ const method = buf.readUInt16LE(p + 10);
41
+ let compSize = buf.readUInt32LE(p + 20);
42
+ let size = buf.readUInt32LE(p + 24);
43
+ const nameLen = buf.readUInt16LE(p + 28);
44
+ const extraLen = buf.readUInt16LE(p + 30);
45
+ const commentLen = buf.readUInt16LE(p + 32);
46
+ let localOffset = buf.readUInt32LE(p + 42);
47
+ const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
48
+
49
+ // ZIP64 extra field carries the real sizes/offset when the 32-bit fields are maxed out.
50
+ let e = p + 46 + nameLen;
51
+ const end = e + extraLen;
52
+ while (e + 4 <= end) {
53
+ const id = buf.readUInt16LE(e);
54
+ const len = buf.readUInt16LE(e + 2);
55
+ if (id === 0x0001) {
56
+ let q = e + 4;
57
+ if (size === 0xffffffff) { size = Number(buf.readBigUInt64LE(q)); q += 8; }
58
+ if (compSize === 0xffffffff) { compSize = Number(buf.readBigUInt64LE(q)); q += 8; }
59
+ if (localOffset === 0xffffffff) { localOffset = Number(buf.readBigUInt64LE(q)); }
60
+ }
61
+ e += 4 + len;
62
+ }
63
+ entries.push({ name, method, compSize, size, localOffset });
64
+ p = end + commentLen;
65
+ }
66
+ return entries;
67
+ }
68
+
69
+ function entryData(buf, entry) {
70
+ const p = entry.localOffset;
71
+ if (buf.readUInt32LE(p) !== 0x04034b50) throw new Error(`bad local header for ${entry.name}`);
72
+ const start = p + 30 + buf.readUInt16LE(p + 26) + buf.readUInt16LE(p + 28);
73
+ const raw = buf.subarray(start, start + entry.compSize);
74
+ if (entry.method === 0) return raw;
75
+ if (entry.method === 8) return zlib.inflateRawSync(raw);
76
+ throw new Error(`unsupported compression ${entry.method} for ${entry.name}`);
77
+ }
78
+
79
+ // Smallest PT_LOAD alignment of a 64-bit little-endian ELF, or null if it is not one.
80
+ export function minLoadAlignment(elf) {
81
+ if (elf.length < 64 || elf.readUInt32BE(0) !== 0x7f454c46) return null;
82
+ if (elf[4] !== 2 || elf[5] !== 1) return null; // ELFCLASS64, little-endian
83
+ const phoff = Number(elf.readBigUInt64LE(32));
84
+ const phentsize = elf.readUInt16LE(54);
85
+ const phnum = elf.readUInt16LE(56);
86
+ let min = null;
87
+ for (let i = 0; i < phnum; i++) {
88
+ const h = phoff + i * phentsize;
89
+ if (h + 56 > elf.length) break;
90
+ if (elf.readUInt32LE(h) !== 1) continue; // PT_LOAD
91
+ const align = Number(elf.readBigUInt64LE(h + 48));
92
+ if (min === null || align < min) min = align;
93
+ }
94
+ return min;
95
+ }
96
+
97
+ // Returns { file, libs: [{ path, align }], misaligned: [...] } for one APK/AAB.
98
+ export function checkArchive(file) {
99
+ const buf = fs.readFileSync(file);
100
+ const libs = [];
101
+ for (const entry of readZipEntries(buf)) {
102
+ const m = entry.name.match(/(?:^|\/)lib\/([^/]+)\/[^/]+\.so$/);
103
+ if (!m || !ABIS_64.includes(m[1])) continue;
104
+ let align = null;
105
+ try {
106
+ align = minLoadAlignment(entryData(buf, entry));
107
+ } catch {
108
+ align = null;
109
+ }
110
+ if (align !== null) libs.push({ path: entry.name, align });
111
+ }
112
+ return { file, libs, misaligned: libs.filter((l) => l.align < PAGE_16K) };
113
+ }
114
+
115
+ // Most recently built release (preferred) or debug artifact under android/app/build/outputs.
116
+ export function findBuiltArtifact(root) {
117
+ const base = path.join(root, 'android', 'app', 'build', 'outputs');
118
+ const found = [];
119
+ const walk = (dir, depth) => {
120
+ if (depth > 4) return;
121
+ let list;
122
+ try {
123
+ list = fs.readdirSync(dir, { withFileTypes: true });
124
+ } catch {
125
+ return;
126
+ }
127
+ for (const e of list) {
128
+ const full = path.join(dir, e.name);
129
+ if (e.isDirectory()) walk(full, depth + 1);
130
+ else if (/\.(apk|aab)$/.test(e.name)) found.push({ file: full, mtime: fs.statSync(full).mtimeMs, release: /release/i.test(full) });
131
+ }
132
+ };
133
+ walk(base, 0);
134
+ found.sort((a, b) => Number(b.release) - Number(a.release) || b.mtime - a.mtime);
135
+ return found[0] ? found[0].file : null;
136
+ }
package/src/interop.js ADDED
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { findPackageDir } from './project.js';
4
+
5
+ // Static checks for native modules that will run through the New Architecture interop layer
6
+ // (packages without their own codegen spec). Verified on a real upgrade: Kotlin @ReactMethods
7
+ // written as `fun x(...) = scope.launch { }` return a kotlinx.coroutines.Job, the interop only
8
+ // accepts void, and the whole module fails to load (a red screen if it is imported at startup).
9
+
10
+ const REACT_METHOD_RETURNS_JOB =
11
+ /@ReactMethod(?!\s*\(\s*isBlockingSynchronousMethod\s*=\s*true)[^\n]*\n(?:\s*@[^\n]*\n)*\s*(?:override\s+)?fun\s+(\w+)\s*\([^)]*\)\s*=\s*(?:[\w.]+\.)?(?:launch|async)\s*[({]/g;
12
+
13
+ function kotlinFiles(dir, out = []) {
14
+ let list;
15
+ try {
16
+ list = fs.readdirSync(dir, { withFileTypes: true });
17
+ } catch {
18
+ return out;
19
+ }
20
+ for (const e of list) {
21
+ const full = path.join(dir, e.name);
22
+ if (e.isDirectory()) {
23
+ if (e.name !== 'build' && e.name !== 'generated') kotlinFiles(full, out);
24
+ } else if (e.name.endsWith('.kt')) {
25
+ out.push(full);
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+
31
+ // Returns [{ name, methods: [...] }] for installed native packages with interop-breaking methods.
32
+ export function interopProblems(project, deps) {
33
+ if (!project.hasNodeModules) return [];
34
+ const out = [];
35
+ for (const dep of deps.filter((d) => d.native)) {
36
+ const dir = findPackageDir(project.root, dep.name);
37
+ if (!dir) continue;
38
+ let manifest = {};
39
+ try {
40
+ manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
41
+ } catch {
42
+ // unreadable
43
+ }
44
+ if (manifest.codegenConfig) continue; // has its own TurboModule spec, no interop involved
45
+ const methods = [];
46
+ for (const file of kotlinFiles(path.join(dir, 'android'))) {
47
+ const text = fs.readFileSync(file, 'utf8');
48
+ for (const m of text.matchAll(REACT_METHOD_RETURNS_JOB)) methods.push(m[1]);
49
+ }
50
+ if (methods.length) out.push({ name: dep.name, version: dep.version, methods });
51
+ }
52
+ return out;
53
+ }
@@ -0,0 +1,61 @@
1
+ // Version-specific breakages that registries do not know about. Each entry was hit in a real
2
+ // upgrade and records where. This list is the part of NativeKeel that grows with every job.
3
+ //
4
+ // Matching: the installed version of `pkg` is within [from, to] (inclusive, either optional),
5
+ // React Native's minor is within [rnFrom, rnTo], and `when` holds:
6
+ // 'always' always
7
+ // 'newArch' the New Architecture is on, or the report says it must be turned on
8
+
9
+ export const KNOWN_ISSUES = [
10
+ {
11
+ id: 'drawer6-reanimated4',
12
+ pkg: '@react-navigation/drawer',
13
+ from: '6.0.0',
14
+ to: '6.99.99',
15
+ rnFrom: 79,
16
+ when: 'always',
17
+ severity: 'medium',
18
+ title: '@react-navigation/drawer 6 cannot run on Reanimated 4, which React Native 0.82+ needs',
19
+ detail: "Its drawer uses useAnimatedGestureHandler, removed in Reanimated 4 (found in lib/module/views/modern/Drawer.js). Reanimated 3 supports React Native up to 0.81, so the hop past 0.81 also means React Navigation 7 for every navigator. Plan that JS migration before the React Native hop.",
20
+ source: 'real upgrade of an RN 0.77 app, 2026-10-03 (source inspection)',
21
+ },
22
+ {
23
+ id: 'rnfs-fork-codegen-eventemitter',
24
+ pkg: '@dr.pogodin/react-native-fs',
25
+ from: '2.37.0',
26
+ rnTo: 77,
27
+ when: 'always',
28
+ severity: 'high',
29
+ title: '@dr.pogodin/react-native-fs 2.37+ breaks pod install on React Native 0.77',
30
+ detail: "Its TurboModule spec uses EventEmitter properties that this React Native version's codegen rejects (UnsupportedModulePropertyParserError, property 'onDownloadBegin'). Pin 2.36.2 until React Native is upgraded. Its peer range says react-native: *, so npm will not warn you.",
31
+ source: 'real upgrade of an RN 0.77 app, 2026-10-03',
32
+ },
33
+ ];
34
+
35
+ // Semver-ish compare on major.minor.patch, ignoring pre-release tags.
36
+ export function compareVersions(a, b) {
37
+ const pa = String(a).split('-')[0].split('.').map(Number);
38
+ const pb = String(b).split('-')[0].split('.').map(Number);
39
+ for (let i = 0; i < 3; i++) {
40
+ const d = (pa[i] || 0) - (pb[i] || 0);
41
+ if (d) return d < 0 ? -1 : 1;
42
+ }
43
+ return 0;
44
+ }
45
+
46
+ export function matchKnownIssues({ deps, rnVersion, newArchOn, newArchRequired }) {
47
+ const rnMinor = rnVersion ? Number(rnVersion.split('.')[1]) : null;
48
+ const out = [];
49
+ for (const issue of KNOWN_ISSUES) {
50
+ const dep = deps.find((d) => d.name === issue.pkg);
51
+ if (!dep || !dep.version) continue;
52
+ if (issue.from && compareVersions(dep.version, issue.from) < 0) continue;
53
+ if (issue.to && compareVersions(dep.version, issue.to) > 0) continue;
54
+ if (rnMinor === null) continue;
55
+ if (issue.rnFrom !== undefined && rnMinor < issue.rnFrom) continue;
56
+ if (issue.rnTo !== undefined && rnMinor > issue.rnTo) continue;
57
+ if (issue.when === 'newArch' && !newArchOn && !newArchRequired) continue;
58
+ out.push({ issue, dep });
59
+ }
60
+ return out;
61
+ }