rn-iso 0.6.0 → 0.6.2

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": "rn-iso",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Isolated React Native dev environments per project/worktree",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -109,9 +109,10 @@ npx rn-iso stop agent-1
109
109
 
110
110
  When the iOS picker fires, sims are sorted by:
111
111
  1. Family (iPhone before iPad before others)
112
- 2. State (booted before shutdown within family)
113
- 3. Usage count (most-used floats up; tracked per UDID across all projects)
114
- 4. Name (alphabetical, stable tiebreak)
112
+ 2. State (booted before shutdown within family, so an already-running sim is reused rather than booting another)
113
+ 3. Runtime version (newest installed iOS runtime first, so `--auto`/agent runs prefer the latest runtime over older ones within the same state)
114
+ 4. Usage count (most-used floats up; tracked per UDID across all projects)
115
+ 5. Name (alphabetical, stable tiebreak)
115
116
 
116
117
  When the Android picker fires, candidates include both AVDs on disk and physical devices currently visible to `adb`. They are sorted by running state (running emulators and connected physical devices first), then physical above AVDs within the same running group, then alphabetically. Physical devices show with a `[physical]` tag. Once selected, a physical device is claimed by serial just like an AVD; `release` clears the claim but never shuts the device down.
117
118
 
@@ -60,10 +60,11 @@ export default function androidCommand(program) {
60
60
  }
61
61
  // With --managed-metro, rn-iso owns Metro: detached so it survives the
62
62
  // invoking shell (agents run builds from finite shells), output to the
63
- // per-project log file, and the build CLI gets --no-packager /
64
- // --no-bundler so it cannot start a second Metro on the same port.
65
- // Without the flag, the build CLI owns Metro as usual (interactive
66
- // bundler UX for humans).
63
+ // per-project log file. The build CLI is then kept from starting its own:
64
+ // bare RN gets --no-packager; expo gets --port and reuses the Metro
65
+ // already listening on that port. ensureMetro waits for /status so the
66
+ // port is bound before the build's reuse check runs. Without the flag,
67
+ // the build CLI owns Metro as usual (interactive bundler UX for humans).
67
68
  if (opts.managedMetro) {
68
69
  const metro = await ensureMetro({ projectPath: root, isExpo, port: proj.metroPort });
69
70
  if (metro.alreadyRunning) {
@@ -72,6 +73,9 @@ export default function androidCommand(program) {
72
73
  setMetro(root, proj.metroPort, metro.pid);
73
74
  console.log(chalk.dim(`Metro started detached (pid ${metro.pid}, port ${proj.metroPort})`));
74
75
  console.log(chalk.dim(`Metro log: ${logFileFor(root)}`));
76
+ if (!metro.ready) {
77
+ console.log(chalk.yellow(`Warning: Metro on port ${proj.metroPort} did not report ready; the build may start its own.`));
78
+ }
75
79
  }
76
80
  } else {
77
81
  const metroAlreadyUp = await isMetroRunning(proj.metroPort);
@@ -56,10 +56,11 @@ export default function iosCommand(program) {
56
56
  }
57
57
  // With --managed-metro, rn-iso owns Metro: detached so it survives the
58
58
  // invoking shell (agents run builds from finite shells), output to the
59
- // per-project log file, and the build CLI gets --no-packager /
60
- // --no-bundler so it cannot start a second Metro on the same port.
61
- // Without the flag, the build CLI owns Metro as usual (interactive
62
- // bundler UX for humans).
59
+ // per-project log file. The build CLI is then kept from starting its own:
60
+ // bare RN gets --no-packager; expo gets --port and reuses the Metro
61
+ // already listening on that port. ensureMetro waits for /status so the
62
+ // port is bound before the build's reuse check runs. Without the flag,
63
+ // the build CLI owns Metro as usual (interactive bundler UX for humans).
63
64
  if (opts.managedMetro) {
64
65
  const metro = await ensureMetro({ projectPath: root, isExpo, port: proj.metroPort });
65
66
  if (metro.alreadyRunning) {
@@ -68,6 +69,9 @@ export default function iosCommand(program) {
68
69
  setMetro(root, proj.metroPort, metro.pid);
69
70
  console.log(chalk.dim(`Metro started detached (pid ${metro.pid}, port ${proj.metroPort})`));
70
71
  console.log(chalk.dim(`Metro log: ${logFileFor(root)}`));
72
+ if (!metro.ready) {
73
+ console.log(chalk.yellow(`Warning: Metro on port ${proj.metroPort} did not report ready; the build may start its own.`));
74
+ }
71
75
  }
72
76
  } else {
73
77
  const metroAlreadyUp = await isMetroRunning(proj.metroPort);
package/src/metro.js CHANGED
@@ -25,8 +25,8 @@ export function buildMetroSpawnArgs({ isExpo, port, extras = [] }) {
25
25
  };
26
26
  }
27
27
 
28
- export async function ensureMetro({ projectPath, isExpo, port, extras = [], detach = true }) {
29
- if (await isMetroRunning(port)) return { alreadyRunning: true, pid: null };
28
+ export async function ensureMetro({ projectPath, isExpo, port, extras = [], detach = true, readyTimeoutMs = 30000 }) {
29
+ if (await isMetroRunning(port)) return { alreadyRunning: true, pid: null, ready: true };
30
30
 
31
31
  const log = logFileFor(projectPath);
32
32
  const fd = openSync(log, 'a');
@@ -40,7 +40,22 @@ export async function ensureMetro({ projectPath, isExpo, port, extras = [], deta
40
40
  env: { ...process.env, RCT_METRO_PORT: String(port) },
41
41
  });
42
42
  if (detach) child.unref();
43
- return { alreadyRunning: false, pid: child.pid };
43
+
44
+ // Wait until the dev server answers /status. The build CLI's "a Metro is
45
+ // already on this port, reuse it" detection only fires once the port is
46
+ // bound, so returning before that races the build into spawning a second
47
+ // Metro. readyTimeoutMs <= 0 skips the wait.
48
+ const ready = readyTimeoutMs > 0 ? await waitForMetroReady(port, readyTimeoutMs) : false;
49
+ return { alreadyRunning: false, pid: child.pid, ready };
50
+ }
51
+
52
+ export async function waitForMetroReady(port, timeoutMs = 30000, intervalMs = 500) {
53
+ const deadline = Date.now() + timeoutMs;
54
+ while (Date.now() < deadline) {
55
+ if (await isMetroRunning(port)) return true;
56
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
57
+ }
58
+ return false;
44
59
  }
45
60
 
46
61
  export function killMetroByPid(pid) {
package/src/runner.js CHANGED
@@ -76,11 +76,17 @@ export function detectScriptCli(scriptBody) {
76
76
  return 'unknown';
77
77
  }
78
78
 
79
- // Flag that stops the build CLI from spawning its own packager. rn-iso owns
80
- // Metro (detached, log file, pid tracked), so the build must not start a
81
- // second one on the same port.
82
- function skipPackagerFlag(cli) {
83
- return cli === 'expo' ? '--no-bundler' : '--no-packager';
79
+ // Flags that stop the build CLI from spawning its own packager when rn-iso owns
80
+ // Metro (detached, log file, pid tracked). Bare RN takes --no-packager. Expo
81
+ // gets NO flag: `expo run` rejects --port together with --no-bundler, and
82
+ // --no-bundler also pins the dev server to 8081 with no override. Instead we
83
+ // pass --port <managed port> and rely on `expo run` detecting the Metro already
84
+ // listening on that port and reusing it (reuseExistingPort) rather than
85
+ // spawning its own. Callers must have the managed Metro listening before the
86
+ // build runs (ensureMetro waits for /status).
87
+ function noPackagerFlags(cli, noPackager) {
88
+ if (!noPackager) return [];
89
+ return cli === 'expo' ? [] : ['--no-packager'];
84
90
  }
85
91
 
86
92
  // iOS run command. Prefers the project's `ios` script if present (the most
@@ -99,15 +105,14 @@ export function buildIosCommand({ projectRoot, packageManager, scriptName, isExp
99
105
  return buildScriptCommand(packageManager, scriptName, [
100
106
  deviceFlag,
101
107
  `--port ${port}`,
102
- ...(noPackager ? [skipPackagerFlag(cli)] : []),
108
+ ...noPackagerFlags(cli, noPackager),
103
109
  ...tail,
104
110
  ]);
105
111
  }
106
112
  }
107
113
  const tailStr = tail.length ? ' ' + tail.join(' ') : '';
108
114
  if (isExpo) {
109
- const skip = noPackager ? ' --no-bundler' : '';
110
- return `npx expo run:ios --device ${udid} --port ${port}${skip}${tailStr}`;
115
+ return `npx expo run:ios --device ${udid} --port ${port}${tailStr}`;
111
116
  }
112
117
  const skip = noPackager ? ' --no-packager' : '';
113
118
  return `npx react-native run-ios --udid ${udid} --port ${port}${skip}${tailStr}`;
@@ -128,15 +133,14 @@ export function buildAndroidCommand({ projectRoot, packageManager, scriptName, i
128
133
  return buildScriptCommand(packageManager, scriptName, [
129
134
  deviceFlag,
130
135
  `--port ${port}`,
131
- ...(noPackager ? [skipPackagerFlag(cli)] : []),
136
+ ...noPackagerFlags(cli, noPackager),
132
137
  ...tail,
133
138
  ]);
134
139
  }
135
140
  }
136
141
  const tailStr = tail.length ? ' ' + tail.join(' ') : '';
137
142
  if (isExpo) {
138
- const skip = noPackager ? ' --no-bundler' : '';
139
- return `npx expo run:android --device "${expoDeviceArg}" --port ${port}${skip}${tailStr}`;
143
+ return `npx expo run:android --device "${expoDeviceArg}" --port ${port}${tailStr}`;
140
144
  }
141
145
  const skip = noPackager ? ' --no-packager' : '';
142
146
  return `RCT_METRO_PORT=${port} npx react-native run-android --device ${serial}${skip}${tailStr}`;
package/src/sim/ios.js CHANGED
@@ -53,14 +53,20 @@ export function sortSims(sims, usage = {}) {
53
53
  const fa = deviceFamilyRank(a.name);
54
54
  const fb = deviceFamilyRank(b.name);
55
55
  if (fa !== fb) return fa - fb;
56
- // 2. State: booted before shutdown (within the same family).
56
+ // 2. State: booted before shutdown (within the same family), so an
57
+ // already-running sim is reused instead of booting another.
57
58
  if (a.state === 'Booted' && b.state !== 'Booted') return -1;
58
59
  if (b.state === 'Booted' && a.state !== 'Booted') return 1;
59
- // 3. Usage count: descending (frequently picked sims float up).
60
+ // 3. Runtime version: newest iOS runtime first, so --auto and agent
61
+ // selection prefer the latest installed runtime over older ones.
62
+ const va = parseRuntimeVersion(a.runtime);
63
+ const vb = parseRuntimeVersion(b.runtime);
64
+ if (va !== vb) return vb.localeCompare(va, undefined, { numeric: true });
65
+ // 4. Usage count: descending (frequently picked sims float up).
60
66
  const ua = usage[a.udid] || 0;
61
67
  const ub = usage[b.udid] || 0;
62
68
  if (ua !== ub) return ub - ua;
63
- // 4. Name: stable alphabetical.
69
+ // 5. Name: stable alphabetical.
64
70
  return a.name.localeCompare(b.name);
65
71
  });
66
72
  }