@react-native-native/cli 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.
package/bin/nativ.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { defineCommand, runMain } = require('citty');
4
+
5
+ const main = defineCommand({
6
+ meta: { name: 'nativ', description: 'React Native Native CLI' },
7
+ subCommands: {
8
+ doctor: () => require('../commands/doctor.js'),
9
+ setup: () => require('../commands/setup.js'),
10
+ build: () => require('../commands/build.js'),
11
+ },
12
+ });
13
+
14
+ runMain(main);
@@ -0,0 +1,38 @@
1
+ const { defineCommand } = require('citty');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ module.exports = defineCommand({
6
+ meta: { name: 'rust', description: 'Compile Rust to static libraries for production' },
7
+ args: {
8
+ platform: {
9
+ type: 'string',
10
+ description: 'Target platform (ios or android)',
11
+ required: true,
12
+ },
13
+ },
14
+ run({ args }) {
15
+ const platform = args.platform;
16
+ if (!['ios', 'android'].includes(platform)) {
17
+ console.error('Error: --platform must be "ios" or "android"');
18
+ process.exit(1);
19
+ }
20
+
21
+ const staticCompiler = path.resolve(
22
+ require.resolve('@react-native-native/nativ-fabric/package.json'),
23
+ '..', 'metro', 'compilers', 'static-compiler.js'
24
+ );
25
+
26
+ console.log(`Building Rust for ${platform}...\n`);
27
+
28
+ try {
29
+ execSync(`node "${staticCompiler}" --platform ${platform} --rust-only`, {
30
+ stdio: 'inherit',
31
+ cwd: process.cwd(),
32
+ });
33
+ } catch (e) {
34
+ console.error(`\nRust build failed for ${platform}`);
35
+ process.exit(1);
36
+ }
37
+ },
38
+ });
@@ -0,0 +1,8 @@
1
+ const { defineCommand } = require('citty');
2
+
3
+ module.exports = defineCommand({
4
+ meta: { name: 'build', description: 'Prebuild native libraries for production' },
5
+ subCommands: {
6
+ rust: () => require('./build-rust.js'),
7
+ },
8
+ });
@@ -0,0 +1,18 @@
1
+ const { defineCommand } = require('citty');
2
+
3
+ module.exports = defineCommand({
4
+ meta: { name: 'doctor', description: 'Check development environment for issues' },
5
+ args: {
6
+ platform: {
7
+ type: 'string',
8
+ description: 'Limit checks to a single platform (ios or android)',
9
+ },
10
+ },
11
+ run({ args }) {
12
+ // Make args available to the script via process.env
13
+ if (args.platform) {
14
+ process.argv.push('--platform', args.platform);
15
+ }
16
+ require('../scripts/doctor.js');
17
+ },
18
+ });
@@ -0,0 +1,8 @@
1
+ const { defineCommand } = require('citty');
2
+
3
+ module.exports = defineCommand({
4
+ meta: { name: 'compose', description: 'Download Compose compiler toolchain (includes Kotlin)' },
5
+ run() {
6
+ require('../scripts/setup-compose.js');
7
+ },
8
+ });
@@ -0,0 +1,8 @@
1
+ const { defineCommand } = require('citty');
2
+
3
+ module.exports = defineCommand({
4
+ meta: { name: 'kotlin', description: 'Download Kotlin compiler toolchain' },
5
+ run() {
6
+ require('../scripts/setup-kotlin.js');
7
+ },
8
+ });
@@ -0,0 +1,15 @@
1
+ const { defineCommand } = require('citty');
2
+
3
+ module.exports = defineCommand({
4
+ meta: { name: 'rust', description: 'Install Rust targets + create Cargo.toml' },
5
+ args: {
6
+ platform: {
7
+ type: 'string',
8
+ description: 'Limit to a single platform (ios or android)',
9
+ },
10
+ },
11
+ run({ args }) {
12
+ if (args.platform) process.argv.push('--platform', args.platform);
13
+ require('../scripts/setup-rust.js');
14
+ },
15
+ });
@@ -0,0 +1,21 @@
1
+ const { defineCommand } = require('citty');
2
+
3
+ module.exports = defineCommand({
4
+ meta: { name: 'setup', description: 'Set up language toolchains' },
5
+ args: {
6
+ platform: {
7
+ type: 'string',
8
+ description: 'Limit to a single platform (ios or android)',
9
+ },
10
+ },
11
+ subCommands: {
12
+ rust: () => require('./setup-rust.js'),
13
+ kotlin: () => require('./setup-kotlin.js'),
14
+ compose: () => require('./setup-compose.js'),
15
+ },
16
+ async run({ rawArgs }) {
17
+ // Only run interactive wizard when no subcommand given
18
+ const sub = rawArgs.find(a => ['rust', 'kotlin', 'compose'].includes(a));
19
+ if (!sub) require('../scripts/setup.js');
20
+ },
21
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@react-native-native/cli",
3
+ "description": "CLI tools for React Native Native — setup, diagnostics, and build commands.",
4
+ "version": "0.1.0",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "nativ": "./bin/nativ.js"
8
+ },
9
+ "dependencies": {
10
+ "citty": "^0.2.2",
11
+ "consola": "^3.4.2"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/react-native-native/react-native-native.git",
16
+ "directory": "packages/cli"
17
+ },
18
+ "keywords": [
19
+ "react-native",
20
+ "native",
21
+ "cli",
22
+ "c++",
23
+ "rust",
24
+ "swift",
25
+ "kotlin",
26
+ "hot-reload",
27
+ "nativ"
28
+ ],
29
+ "author": "Kim Brandwijk <kim.brandwijk@gmail.com>",
30
+ "homepage": "https://react-native-native.github.io",
31
+ "bugs": {
32
+ "url": "https://github.com/react-native-native/react-native-native/issues"
33
+ }
34
+ }
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * doctor.js — Scans development environment for React Native Native prerequisites.
5
+ *
6
+ * Checks are language-aware: only shows Rust checks if Cargo.toml exists,
7
+ * Kotlin if .kt files exist, Compose if @Composable is used, etc.
8
+ *
9
+ * Run: npx nativ doctor
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+
16
+ const projectRoot = process.cwd();
17
+ const platformArg = process.argv.find((a, i) => i > 0 && process.argv[i - 1] === '--platform') || 'all';
18
+ const wantIOS = platformArg === 'all' || platformArg === 'ios';
19
+ const wantAndroid = platformArg === 'all' || platformArg === 'android';
20
+ let issues = 0;
21
+
22
+ // ── Helpers ───────────────────────────────────────────────────────────
23
+
24
+ function run(cmd) {
25
+ try {
26
+ return execSync(cmd, { encoding: 'utf8', stdio: 'pipe', timeout: 5000 }).trim();
27
+ } catch { return null; }
28
+ }
29
+
30
+ function ok(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
31
+ function fail(msg, fix) { issues++; console.log(` \x1b[31m✗\x1b[0m ${msg}`); if (fix) console.log(` ${fix}`); }
32
+ function skip(msg) { console.log(` \x1b[90m○\x1b[0m ${msg}`); }
33
+ function header(title) { console.log(`\n\x1b[1m${title}:\x1b[0m`); }
34
+
35
+ function findFiles(exts) {
36
+ const results = [];
37
+ const ignore = ['node_modules', '.nativ', 'ios', 'android', 'vendor', '.git'];
38
+ function walk(dir, depth) {
39
+ if (depth > 3) return;
40
+ let entries;
41
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
42
+ for (const entry of entries) {
43
+ if (ignore.includes(entry.name)) continue;
44
+ const full = path.join(dir, entry.name);
45
+ if (entry.isDirectory()) walk(full, depth + 1);
46
+ else if (exts.some(ext => entry.name.endsWith(ext))) results.push(full);
47
+ }
48
+ }
49
+ walk(projectRoot, 0);
50
+ return results;
51
+ }
52
+
53
+ // ── Detect opted-in languages ─────────────────────────────────────────
54
+
55
+ const hasCargoToml = fs.existsSync(path.join(projectRoot, 'Cargo.toml'));
56
+ const rsFiles = findFiles(['.rs']);
57
+ const ktFiles = findFiles(['.kt']);
58
+ const cppFiles = findFiles(['.cpp', '.cc', '.mm']);
59
+ const swiftFiles = findFiles(['.swift']);
60
+ const hasCompose = ktFiles.some(f => {
61
+ try { return fs.readFileSync(f, 'utf8').includes('@Composable'); } catch { return false; }
62
+ });
63
+
64
+ // ── Environment ───────────────────────────────────────────────────────
65
+
66
+ console.log('\n\x1b[1mreact-native-native doctor\x1b[0m');
67
+ console.log('───────────────────────────────────────────');
68
+
69
+ header('Environment');
70
+
71
+ const nodeVersion = run('node --version');
72
+ if (nodeVersion) ok(`Node.js ${nodeVersion}`);
73
+ else fail('Node.js not found');
74
+
75
+ // React Native version
76
+ try {
77
+ const rnPkg = JSON.parse(fs.readFileSync(
78
+ require.resolve('react-native/package.json', { paths: [projectRoot] }), 'utf8'));
79
+ ok(`react-native ${rnPkg.version}`);
80
+ } catch { skip('react-native not found in node_modules'); }
81
+
82
+ // nativ-fabric version
83
+ try {
84
+ const nfPkg = JSON.parse(fs.readFileSync(
85
+ require.resolve('@react-native-native/nativ-fabric/package.json', { paths: [projectRoot] }), 'utf8'));
86
+ ok(`@react-native-native/nativ-fabric ${nfPkg.version}`);
87
+ } catch { fail('@react-native-native/nativ-fabric not installed', 'Run: npm install @react-native-native/nativ-fabric'); }
88
+
89
+ // Config file
90
+ const configPath = path.join(projectRoot, '.nativ/nativ.config.json');
91
+ if (fs.existsSync(configPath)) {
92
+ ok('.nativ/nativ.config.json found');
93
+ } else {
94
+ skip('.nativ/nativ.config.json not found (created by setup commands)');
95
+ }
96
+
97
+ // .gitignore — .nativ/ should be ignored
98
+ {
99
+ let gitignorePath = null;
100
+ try {
101
+ let dir = projectRoot;
102
+ const gitRoot = run(`git -C "${projectRoot}" rev-parse --show-toplevel`);
103
+ const stopAt = gitRoot || projectRoot;
104
+ while (dir.length >= stopAt.length) {
105
+ const candidate = path.join(dir, '.gitignore');
106
+ if (fs.existsSync(candidate)) { gitignorePath = candidate; break; }
107
+ const parent = path.dirname(dir);
108
+ if (parent === dir) break;
109
+ dir = parent;
110
+ }
111
+ } catch {}
112
+ if (gitignorePath) {
113
+ const content = fs.readFileSync(gitignorePath, 'utf8');
114
+ if (content.includes('.nativ')) ok('.nativ/ in .gitignore');
115
+ else fail('.nativ/ not in .gitignore', 'Run: npx nativ setup');
116
+ } else {
117
+ fail('No .gitignore found', 'Run: npx nativ setup');
118
+ }
119
+ }
120
+
121
+ // tsconfig.json — rootDirs for .d.ts resolution
122
+ try {
123
+ const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
124
+ if (fs.existsSync(tsconfigPath)) {
125
+ const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));
126
+ const rootDirs = tsconfig.compilerOptions?.rootDirs || [];
127
+ if (rootDirs.some(d => d.includes('.nativ/typings'))) ok('tsconfig rootDirs includes .nativ/typings');
128
+ else fail('tsconfig missing rootDirs for .nativ/typings', 'Run: npx nativ setup');
129
+ }
130
+ } catch {}
131
+
132
+ // ── C++ ───────────────────────────────────────────────────────────────
133
+
134
+ const mmFiles = cppFiles.filter(f => f.endsWith('.mm'));
135
+ const pureC = cppFiles.filter(f => !f.endsWith('.mm'));
136
+ header(wantIOS && mmFiles.length > 0 ? 'C++ / ObjC++' : 'C++');
137
+ if (cppFiles.length > 0) skip(`${cppFiles.length} file${cppFiles.length > 1 ? 's' : ''} found`);
138
+ else skip(wantIOS ? 'No .cpp/.mm files found' : 'No .cpp files found');
139
+ {
140
+ if (wantIOS) {
141
+ const clangVersion = run('clang++ --version');
142
+ if (clangVersion) {
143
+ const m = clangVersion.match(/Apple clang version ([\d.]+)/i) || clangVersion.match(/clang version ([\d.]+)/i);
144
+ ok(`clang${m ? ' ' + m[1] : ''} (Xcode)`);
145
+ } else {
146
+ fail('clang++ not found', 'Install Xcode command line tools: xcode-select --install');
147
+ }
148
+ }
149
+ if (wantAndroid) {
150
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
151
+ if (androidHome) {
152
+ const ndkDir = path.join(androidHome, 'ndk');
153
+ try {
154
+ const versions = fs.readdirSync(ndkDir).sort();
155
+ if (versions.length > 0) {
156
+ const toolchain = path.join(ndkDir, versions[versions.length - 1], 'toolchains/llvm/prebuilt');
157
+ const hosts = fs.readdirSync(toolchain);
158
+ if (hosts.length > 0) {
159
+ const clangPath = path.join(toolchain, hosts[0], 'bin/aarch64-linux-android24-clang++');
160
+ if (fs.existsSync(clangPath)) ok(`clang (NDK ${versions[versions.length - 1]})`);
161
+ else fail('NDK clang++ not found');
162
+ }
163
+ }
164
+ } catch {
165
+ fail('NDK clang++ not found', 'Install Android NDK via SDK Manager');
166
+ }
167
+ } else if (pureC.length > 0) {
168
+ fail('ANDROID_HOME not set — needed for C++ on Android');
169
+ }
170
+ }
171
+ }
172
+
173
+ // ── Swift (iOS only) ──────────────────────────────────────────────────
174
+
175
+ if (wantIOS) {
176
+ header('Swift');
177
+ if (swiftFiles.length > 0) skip(`${swiftFiles.length} file${swiftFiles.length > 1 ? 's' : ''} found`);
178
+ else skip('No .swift files found');
179
+ const swiftVersion = run('swiftc --version');
180
+ if (swiftVersion) {
181
+ const m = swiftVersion.match(/Swift version ([\d.]+)/);
182
+ ok(`swiftc${m ? ' ' + m[1] : ''}`);
183
+ } else {
184
+ fail('swiftc not found', 'Install Xcode: xcode-select --install');
185
+ }
186
+ }
187
+
188
+ // ── Rust ──────────────────────────────────────────────────────────────
189
+
190
+ header('Rust');
191
+ if (rsFiles.length > 0) skip(`${rsFiles.length} file${rsFiles.length > 1 ? 's' : ''} found`);
192
+ else skip('No .rs files found');
193
+
194
+ if (hasCargoToml) {
195
+ ok('Cargo.toml found');
196
+ try {
197
+ const cargo = fs.readFileSync(path.join(projectRoot, 'Cargo.toml'), 'utf8');
198
+ if (cargo.includes('nativ-core')) ok('nativ-core dependency');
199
+ else fail('nativ-core not in Cargo.toml', 'Run: npx nativ setup rust');
200
+ } catch {}
201
+ if (fs.existsSync(path.join(projectRoot, '.nativ/lib.rs'))) ok('.nativ/lib.rs');
202
+ else fail('.nativ/lib.rs missing', 'Run: npx nativ setup rust');
203
+ } else {
204
+ skip('No Cargo.toml — run setup-rust to get started');
205
+ }
206
+
207
+ {
208
+ const rustcVersion = run('rustc --version');
209
+ if (rustcVersion) {
210
+ const m = rustcVersion.match(/rustc ([\d.]+)/);
211
+ ok(`rustc${m ? ' ' + m[1] : ''}`);
212
+ } else {
213
+ fail('rustc not found', 'Install: curl --proto \'=https\' --tlsv1.2 -sSf https://sh.rustup.rs | sh');
214
+ }
215
+
216
+ const installed = run('rustup target list --installed') || '';
217
+ const targets = [
218
+ ...(wantIOS ? [
219
+ ['aarch64-apple-ios', 'iOS device'],
220
+ ['aarch64-apple-ios-sim', 'iOS simulator'],
221
+ ] : []),
222
+ ...(wantAndroid ? [
223
+ ['aarch64-linux-android', 'Android arm64'],
224
+ ['armv7-linux-androideabi', 'Android armv7'],
225
+ ['x86_64-linux-android', 'Android x86_64'],
226
+ ] : []),
227
+ ];
228
+ let missingTargets = false;
229
+ for (const [target, label] of targets) {
230
+ if (installed.includes(target)) ok(`${target}`);
231
+ else { fail(`${target} (${label})`); missingTargets = true; }
232
+ }
233
+ if (missingTargets) console.log(` Run: npx nativ setup rust`);
234
+
235
+ // NDK linker (needed for Android Rust cross-compilation)
236
+ if (wantAndroid) {
237
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
238
+ if (androidHome) {
239
+ const ndkDir = path.join(androidHome, 'ndk');
240
+ try {
241
+ const versions = fs.readdirSync(ndkDir).sort();
242
+ if (versions.length > 0) {
243
+ const toolchain = path.join(ndkDir, versions[versions.length - 1], 'toolchains/llvm/prebuilt');
244
+ const hosts = fs.readdirSync(toolchain);
245
+ if (hosts.length > 0) {
246
+ const linker = path.join(toolchain, hosts[0], 'bin/aarch64-linux-android24-clang');
247
+ if (fs.existsSync(linker)) ok(`NDK linker (${versions[versions.length - 1]})`);
248
+ else fail('NDK linker not found');
249
+ }
250
+ }
251
+ } catch {
252
+ fail('NDK not found', 'Install Android NDK via SDK Manager');
253
+ }
254
+ } else {
255
+ fail('ANDROID_HOME not set', 'Needed for Rust Android cross-compilation');
256
+ }
257
+ }
258
+ }
259
+
260
+ // ── Kotlin + Compose (Android only) ───────────────────────────────────
261
+
262
+ if (wantAndroid) {
263
+ header('Kotlin');
264
+ if (ktFiles.length > 0) skip(`${ktFiles.length} file${ktFiles.length > 1 ? 's' : ''} found`);
265
+ else skip('No .kt files found');
266
+
267
+ let ktVersion = null;
268
+ try {
269
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
270
+ ktVersion = config.kotlin?.version;
271
+ } catch {}
272
+ if (ktVersion) ok(`Kotlin version: ${ktVersion}`);
273
+ else fail('Kotlin version not configured', 'Run: npx nativ setup kotlin');
274
+
275
+ const gradleCache = path.join(process.env.HOME || '', '.gradle/caches/modules-2/files-2.1');
276
+ const localCache = path.join(projectRoot, '.nativ/kotlin-cache');
277
+ const checkJar = (group, artifact, label) => {
278
+ const dir = path.join(gradleCache, group, artifact);
279
+ try {
280
+ const found = execSync(
281
+ `find "${dir}" -name "*.jar" -not -name "*sources*" -not -name "*javadoc*" 2>/dev/null | head -1`,
282
+ { encoding: 'utf8', stdio: 'pipe' }
283
+ ).trim();
284
+ if (found) { ok(label); return; }
285
+ } catch {}
286
+ try {
287
+ const files = fs.readdirSync(localCache).filter(f => f.startsWith(artifact) && f.endsWith('.jar'));
288
+ if (files.length > 0) { ok(`${label} (local cache)`); return; }
289
+ } catch {}
290
+ fail(`${label} not found`, 'Run: npx nativ setup kotlin');
291
+ };
292
+ checkJar('org.jetbrains.kotlin', 'kotlin-compiler-embeddable', 'kotlin-compiler-embeddable');
293
+ checkJar('org.jetbrains.kotlin', 'kotlin-stdlib', 'kotlin-stdlib');
294
+
295
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT
296
+ || path.join(process.env.HOME || '', 'Library/Android/sdk');
297
+ let hasAndroidJar = false;
298
+ try {
299
+ const platforms = path.join(androidHome, 'platforms');
300
+ const versions = fs.readdirSync(platforms).sort();
301
+ if (versions.length > 0) {
302
+ const jar = path.join(platforms, versions[versions.length - 1], 'android.jar');
303
+ if (fs.existsSync(jar)) { ok(`android.jar (${versions[versions.length - 1]})`); hasAndroidJar = true; }
304
+ }
305
+ } catch {}
306
+ if (!hasAndroidJar) {
307
+ if (fs.existsSync(path.join(localCache, 'android.jar'))) ok('android.jar (local cache)');
308
+ else fail('android.jar not found', 'Run: npx nativ setup kotlin');
309
+ }
310
+
311
+ let hasD8 = false;
312
+ try {
313
+ const btDir = path.join(androidHome, 'build-tools');
314
+ if (fs.existsSync(btDir) && fs.readdirSync(btDir).length > 0) { ok('d8 (build-tools)'); hasD8 = true; }
315
+ } catch {}
316
+ if (!hasD8) {
317
+ if (fs.existsSync(path.join(localCache, 'd8.jar'))) ok('d8.jar (local cache)');
318
+ else fail('d8 not found', 'Run: npx nativ setup kotlin');
319
+ }
320
+
321
+ // ── Jetpack Compose ─────────────────────────────────────────────────
322
+ header('Jetpack Compose');
323
+ if (hasCompose) skip('@Composable usage found');
324
+ else skip('No @Composable files found');
325
+
326
+ const pretransformDir = path.join(projectRoot, '.nativ/compose-pretransform');
327
+ const checkComposeFile = (pattern, label) => {
328
+ try {
329
+ const files = fs.readdirSync(pretransformDir).filter(f => f.includes(pattern));
330
+ if (files.length > 0) { ok(label); return; }
331
+ } catch {}
332
+ fail(label, 'Run: npx nativ setup compose');
333
+ };
334
+ checkComposeFile('compose-pretransform', 'compose-pretransform JAR');
335
+ checkComposeFile('compose-wrappers', 'compose-wrappers JAR');
336
+ checkComposeFile('compose-host', 'compose-host JAR');
337
+ checkComposeFile('kotlin-compiler-', 'Non-embeddable Kotlin compiler');
338
+ }
339
+
340
+ // ── Android ───────────────────────────────────────────────────────────
341
+
342
+ // ── iOS ───────────────────────────────────────────────────────────────
343
+
344
+ if (wantIOS) {
345
+ header('iOS');
346
+
347
+ // Xcode
348
+ const xcodeVersion = run('xcodebuild -version 2>/dev/null');
349
+ if (xcodeVersion) {
350
+ const m = xcodeVersion.match(/Xcode ([\d.]+)/);
351
+ ok(`Xcode${m ? ' ' + m[1] : ''}`);
352
+ } else {
353
+ fail('Xcode not found', 'Install from the App Store');
354
+ }
355
+
356
+ // Team ID + signing identity
357
+ let teamId = null;
358
+ try {
359
+ const appJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'app.json'), 'utf8'));
360
+ teamId = appJson?.expo?.ios?.appleTeamId;
361
+ } catch {}
362
+
363
+ if (!teamId) {
364
+ // Try pbxproj
365
+ try {
366
+ const pbx = run(`find "${projectRoot}/ios" -name "project.pbxproj" -maxdepth 3 2>/dev/null`);
367
+ if (pbx) {
368
+ const content = fs.readFileSync(pbx.split('\n')[0], 'utf8');
369
+ const m = content.match(/DEVELOPMENT_TEAM\s*=\s*(\w+)/);
370
+ if (m) teamId = m[1];
371
+ }
372
+ } catch {}
373
+ }
374
+
375
+ if (teamId) {
376
+ ok(`Team ID: ${teamId} (from app.json)`);
377
+
378
+ // Find matching signing identity
379
+ const identities = run('security find-identity -v -p codesigning') || '';
380
+ let foundIdentity = null;
381
+
382
+ const entries = [...identities.matchAll(/([A-F0-9]{40})\s+"([^"]+)"/g)];
383
+ for (const [, , name] of entries) {
384
+ try {
385
+ const subject = run(
386
+ `security find-certificate -c "${name}" -p 2>/dev/null | openssl x509 -noout -subject 2>/dev/null`
387
+ ) || '';
388
+ if (subject.includes(`OU=${teamId}`)) {
389
+ foundIdentity = name;
390
+ break;
391
+ }
392
+ } catch {}
393
+ }
394
+
395
+ if (foundIdentity) {
396
+ ok(`Signing: ${foundIdentity}`);
397
+ } else {
398
+ fail(`No signing identity for team ${teamId}`,
399
+ 'Open Xcode → Settings → Accounts → download certificates');
400
+ }
401
+ } else {
402
+ fail('No appleTeamId in app.json',
403
+ 'Add "appleTeamId" to expo.ios in app.json for code signing');
404
+ }
405
+ }
406
+
407
+ // ── Production ────────────────────────────────────────────────────────
408
+
409
+ header('Production');
410
+
411
+ if (fs.existsSync(path.join(projectRoot, 'ReactNativeNativeUserCode.podspec'))) {
412
+ ok('ReactNativeNativeUserCode.podspec found');
413
+ } else {
414
+ skip('ReactNativeNativeUserCode.podspec not found (needed for iOS production builds)');
415
+ }
416
+
417
+ // Check Podfile reference
418
+ try {
419
+ const podfile = fs.readFileSync(path.join(projectRoot, 'ios/Podfile'), 'utf8');
420
+ if (podfile.includes('ReactNativeNativeUserCode')) {
421
+ ok('Podfile includes ReactNativeNativeUserCode');
422
+ } else {
423
+ skip('Podfile missing ReactNativeNativeUserCode pod');
424
+ }
425
+ } catch {
426
+ skip('ios/Podfile not found (CNG mode — generated at prebuild)');
427
+ }
428
+
429
+ // ── Summary ───────────────────────────────────────────────────────────
430
+
431
+ console.log('\n───────────────────────────────────────────');
432
+ if (issues === 0) {
433
+ console.log('\x1b[32mAll checks passed!\x1b[0m\n');
434
+ } else {
435
+ console.log(`\x1b[31m${issues} issue${issues > 1 ? 's' : ''} found\x1b[0m\n`);
436
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * project-config.js — Patches project files for React Native Native.
3
+ *
4
+ * - Adds .nativ/ to nearest .gitignore
5
+ * - Adds rootDirs to tsconfig.json for .d.ts resolution
6
+ *
7
+ * Called by `npx nativ setup` and individual setup commands.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { execSync } = require('child_process');
13
+
14
+ function configureProject(projectRoot) {
15
+ // ── .gitignore ──────────────────────────────────────────────────────
16
+ try {
17
+ let gitignorePath = null;
18
+ let dir = projectRoot;
19
+ let gitRoot = projectRoot;
20
+ try {
21
+ gitRoot = execSync('git rev-parse --show-toplevel', {
22
+ encoding: 'utf8', stdio: 'pipe', cwd: projectRoot,
23
+ }).trim();
24
+ } catch {}
25
+
26
+ while (dir.length >= gitRoot.length) {
27
+ const candidate = path.join(dir, '.gitignore');
28
+ if (fs.existsSync(candidate)) { gitignorePath = candidate; break; }
29
+ const parent = path.dirname(dir);
30
+ if (parent === dir) break;
31
+ dir = parent;
32
+ }
33
+ if (!gitignorePath) gitignorePath = path.join(projectRoot, '.gitignore');
34
+
35
+ const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
36
+ if (!gitignore.includes('.nativ')) {
37
+ fs.appendFileSync(gitignorePath, '\n# React Native Native build cache\n.nativ/\n');
38
+ console.log('✓ Added .nativ/ to .gitignore');
39
+ }
40
+ } catch {}
41
+
42
+ // ── tsconfig.json ───────────────────────────────────────────────────
43
+ try {
44
+ const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
45
+ if (fs.existsSync(tsconfigPath)) {
46
+ const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));
47
+ const rootDirs = tsconfig.compilerOptions?.rootDirs || [];
48
+ if (!rootDirs.some(d => d.includes('.nativ/typings'))) {
49
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
50
+ tsconfig.compilerOptions.rootDirs = ['.', '.nativ/typings'];
51
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + '\n');
52
+ console.log('✓ Added rootDirs to tsconfig.json');
53
+ }
54
+ }
55
+ } catch {}
56
+ }
57
+
58
+ module.exports = { configureProject };