rn-iso 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.
Files changed (42) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/CLAUDE.md +178 -0
  3. package/README.md +90 -0
  4. package/bin/cli.js +35 -0
  5. package/docs/plans/2026-04-25-rn-iso-implementation.md +2653 -0
  6. package/docs/specs/2026-04-25-rn-iso-design.md +282 -0
  7. package/package.json +20 -0
  8. package/skill/SKILL.md +112 -0
  9. package/src/commands/android.js +112 -0
  10. package/src/commands/device.js +43 -0
  11. package/src/commands/ios.js +210 -0
  12. package/src/commands/logs.js +28 -0
  13. package/src/commands/prune.js +57 -0
  14. package/src/commands/release.js +51 -0
  15. package/src/commands/reserve.js +176 -0
  16. package/src/commands/shutdown.js +41 -0
  17. package/src/commands/start.js +43 -0
  18. package/src/commands/status.js +60 -0
  19. package/src/commands/stop.js +51 -0
  20. package/src/commands/unreserve.js +57 -0
  21. package/src/config.js +221 -0
  22. package/src/exec.js +31 -0
  23. package/src/metro.js +73 -0
  24. package/src/ports.js +50 -0
  25. package/src/project.js +186 -0
  26. package/src/runner.js +136 -0
  27. package/src/sim/android.js +103 -0
  28. package/src/sim/ios.js +128 -0
  29. package/test/config.test.js +208 -0
  30. package/test/exec.test.js +26 -0
  31. package/test/fixtures/sample-bare-project/android/app/build.gradle +6 -0
  32. package/test/fixtures/sample-bare-project/ios/SampleApp.xcodeproj/project.pbxproj +10 -0
  33. package/test/fixtures/sample-bare-project/package.json +4 -0
  34. package/test/fixtures/sample-expo-project/app.json +6 -0
  35. package/test/fixtures/sample-expo-project/package.json +4 -0
  36. package/test/fixtures/sample-expo-project/src/.keep +0 -0
  37. package/test/metro.test.js +34 -0
  38. package/test/ports.test.js +76 -0
  39. package/test/project.test.js +109 -0
  40. package/test/runner.test.js +209 -0
  41. package/test/sim-android.test.js +140 -0
  42. package/test/sim-ios.test.js +168 -0
@@ -0,0 +1,168 @@
1
+ import { test, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { setExecutor, resetExecutor } from '../src/exec.js';
7
+ import { parseSimctlList, selectIosDevice, listAllIosSims, listBootedIosSims, sortSims, deviceFamilyRank } from '../src/sim/ios.js';
8
+
9
+ let tmpHome;
10
+
11
+ beforeEach(() => {
12
+ tmpHome = mkdtempSync(join(tmpdir(), 'rn-iso-test-'));
13
+ process.env.RN_ISO_HOME = tmpHome;
14
+ });
15
+
16
+ afterEach(() => {
17
+ rmSync(tmpHome, { recursive: true, force: true });
18
+ delete process.env.RN_ISO_HOME;
19
+ resetExecutor();
20
+ });
21
+
22
+ const SIMCTL_OUTPUT = JSON.stringify({
23
+ devices: {
24
+ 'com.apple.CoreSimulator.SimRuntime.iOS-17-2': [
25
+ { udid: 'UDID-A', name: 'iPhone 15', state: 'Booted', isAvailable: true },
26
+ { udid: 'UDID-B', name: 'iPhone 15 Pro', state: 'Shutdown', isAvailable: true },
27
+ { udid: 'UDID-C', name: 'iPhone 14', state: 'Booted', isAvailable: true },
28
+ ],
29
+ 'com.apple.CoreSimulator.SimRuntime.iOS-16-0': [
30
+ { udid: 'UDID-OLD', name: 'iPhone 13', state: 'Shutdown', isAvailable: false },
31
+ ],
32
+ },
33
+ });
34
+
35
+ test('parseSimctlList flattens devices and filters unavailable', () => {
36
+ const sims = parseSimctlList(SIMCTL_OUTPUT);
37
+ assert.equal(sims.length, 3);
38
+ assert.deepEqual(sims.map(s => s.udid).sort(), ['UDID-A', 'UDID-B', 'UDID-C']);
39
+ });
40
+
41
+ test('parseSimctlList includes runtime in each entry', () => {
42
+ const sims = parseSimctlList(SIMCTL_OUTPUT);
43
+ const a = sims.find(s => s.udid === 'UDID-A');
44
+ assert.equal(a.runtime, 'com.apple.CoreSimulator.SimRuntime.iOS-17-2');
45
+ });
46
+
47
+ test('listAllIosSims uses simctl via executor', () => {
48
+ setExecutor({
49
+ run: (cmd) => {
50
+ assert.match(cmd, /xcrun simctl list devices --json/);
51
+ return SIMCTL_OUTPUT;
52
+ },
53
+ runQuiet: () => null,
54
+ spawn: () => null,
55
+ });
56
+ const sims = listAllIosSims();
57
+ assert.equal(sims.length, 3);
58
+ });
59
+
60
+ test('listBootedIosSims filters by state', () => {
61
+ setExecutor({
62
+ run: () => SIMCTL_OUTPUT,
63
+ runQuiet: () => null,
64
+ spawn: () => null,
65
+ });
66
+ const booted = listBootedIosSims();
67
+ assert.deepEqual(booted.map(s => s.udid).sort(), ['UDID-A', 'UDID-C']);
68
+ });
69
+
70
+ test('selectIosDevice prefers existing assignment when sim still exists', () => {
71
+ setExecutor({ run: () => SIMCTL_OUTPUT, runQuiet: () => null, spawn: () => null });
72
+ const result = selectIosDevice({
73
+ existingUdid: 'UDID-B',
74
+ claimedUdids: [],
75
+ });
76
+ assert.deepEqual(result, { kind: 'reuse', udid: 'UDID-B', state: 'Shutdown' });
77
+ });
78
+
79
+ test('selectIosDevice ignores existing assignment when sim no longer exists', () => {
80
+ setExecutor({ run: () => SIMCTL_OUTPUT, runQuiet: () => null, spawn: () => null });
81
+ const result = selectIosDevice({
82
+ existingUdid: 'GHOST-UDID',
83
+ claimedUdids: [],
84
+ });
85
+ assert.equal(result.kind, 'allocate');
86
+ assert.ok(Array.isArray(result.candidates));
87
+ });
88
+
89
+ test('selectIosDevice returns all unclaimed sims, booted first', () => {
90
+ // SIMCTL_OUTPUT: UDID-A iPhone 15 booted, UDID-B iPhone 15 Pro shutdown, UDID-C iPhone 14 booted.
91
+ setExecutor({ run: () => SIMCTL_OUTPUT, runQuiet: () => null, spawn: () => null });
92
+ const result = selectIosDevice({
93
+ existingUdid: null,
94
+ claimedUdids: [],
95
+ });
96
+ assert.equal(result.kind, 'allocate');
97
+ // Booted sims first (sorted by name within state), then shutdown sims.
98
+ assert.deepEqual(result.candidates.map(s => s.udid), ['UDID-C', 'UDID-A', 'UDID-B']);
99
+ });
100
+
101
+ test('selectIosDevice excludes claimed sims from candidates', () => {
102
+ setExecutor({ run: () => SIMCTL_OUTPUT, runQuiet: () => null, spawn: () => null });
103
+ const result = selectIosDevice({
104
+ existingUdid: null,
105
+ claimedUdids: ['UDID-A', 'UDID-C'],
106
+ });
107
+ assert.equal(result.kind, 'allocate');
108
+ assert.deepEqual(result.candidates.map(s => s.udid), ['UDID-B']);
109
+ });
110
+
111
+ test('selectIosDevice returns needsBoot only when ALL sims are claimed', () => {
112
+ setExecutor({ run: () => SIMCTL_OUTPUT, runQuiet: () => null, spawn: () => null });
113
+ const result = selectIosDevice({
114
+ existingUdid: null,
115
+ claimedUdids: ['UDID-A', 'UDID-B', 'UDID-C'],
116
+ });
117
+ assert.equal(result.kind, 'needsBoot');
118
+ });
119
+
120
+ test('parseRuntimeVersion extracts major.minor from runtime id', async () => {
121
+ const { parseRuntimeVersion } = await import('../src/sim/ios.js');
122
+ assert.equal(parseRuntimeVersion('com.apple.CoreSimulator.SimRuntime.iOS-26-2'), '26.2');
123
+ assert.equal(parseRuntimeVersion('com.apple.CoreSimulator.SimRuntime.iOS-18'), '18');
124
+ assert.equal(parseRuntimeVersion('weird-id'), 'weird-id');
125
+ });
126
+
127
+ test('parseSimctlList drops non-iOS runtimes (watchOS, tvOS, visionOS)', () => {
128
+ const out = JSON.stringify({
129
+ devices: {
130
+ 'com.apple.CoreSimulator.SimRuntime.iOS-26-2': [
131
+ { udid: 'IOS-1', name: 'iPhone 17', state: 'Booted', isAvailable: true },
132
+ ],
133
+ 'com.apple.CoreSimulator.SimRuntime.watchOS-11-0': [
134
+ { udid: 'WATCH-1', name: 'Apple Watch S10', state: 'Booted', isAvailable: true },
135
+ ],
136
+ 'com.apple.CoreSimulator.SimRuntime.tvOS-18-0': [
137
+ { udid: 'TV-1', name: 'Apple TV 4K', state: 'Booted', isAvailable: true },
138
+ ],
139
+ 'com.apple.CoreSimulator.SimRuntime.xrOS-2-0': [
140
+ { udid: 'VISION-1', name: 'Apple Vision Pro', state: 'Booted', isAvailable: true },
141
+ ],
142
+ },
143
+ });
144
+ const sims = parseSimctlList(out);
145
+ assert.deepEqual(sims.map(s => s.udid), ['IOS-1']);
146
+ });
147
+
148
+ test('deviceFamilyRank ranks iPhone < iPad < other', () => {
149
+ assert.equal(deviceFamilyRank('iPhone 17 Pro'), 0);
150
+ assert.equal(deviceFamilyRank('iPad Pro 11-inch'), 1);
151
+ assert.equal(deviceFamilyRank('Apple TV'), 2);
152
+ });
153
+
154
+ test('sortSims orders by family, then state, then usage, then name', () => {
155
+ const sims = [
156
+ { udid: 'A', name: 'iPad Pro', state: 'Booted', runtime: 'r' },
157
+ { udid: 'B', name: 'iPhone 17 Pro', state: 'Shutdown', runtime: 'r' },
158
+ { udid: 'C', name: 'iPhone 16 Pro', state: 'Booted', runtime: 'r' },
159
+ { udid: 'D', name: 'iPhone 15 Pro', state: 'Booted', runtime: 'r' },
160
+ ];
161
+ // Without usage: iPhones first (booted before shutdown), then iPad.
162
+ // C and D are both iPhone+booted with usage 0 -> alpha sort: D ("15 Pro") before C ("16 Pro").
163
+ let sorted = sortSims(sims);
164
+ assert.deepEqual(sorted.map(s => s.udid), ['D', 'C', 'B', 'A']);
165
+ // With C used 5 times: C floats above D within iPhone+booted.
166
+ sorted = sortSims(sims, { C: 5 });
167
+ assert.deepEqual(sorted.map(s => s.udid), ['C', 'D', 'B', 'A']);
168
+ });