rn-iso 0.5.0 → 0.6.1
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/README.md +1 -0
- package/bin/cli.js +2 -0
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
- package/src/commands/android.js +8 -4
- package/src/commands/ios.js +8 -4
- package/src/commands/prune.js +44 -0
- package/src/config.js +20 -0
- package/src/metro.js +18 -3
- package/src/ports.js +5 -0
- package/src/runner.js +15 -11
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ All commands below take the same `npx rn-iso` prefix.
|
|
|
51
51
|
| `start` | Start Metro detached, no platform action |
|
|
52
52
|
| `stop [<port>\|<shortcut>\|<path>]` | Kill Metro. No arg = current project; pass a port (e.g. 8083), a project shortcut (label or unique basename), or an absolute path. |
|
|
53
53
|
| `logs [<port>\|<shortcut>\|<path>] [-n <lines>] [--follow]` | Print the managed Metro log (bundle progress, resolution errors, client logs). |
|
|
54
|
+
| `prune` | Remove entries for deleted project directories, freeing their devices and ports. |
|
|
54
55
|
| `device [--platform ios\|android] [--json]` | Print the assigned device target |
|
|
55
56
|
| `status` | Show all projects' state |
|
|
56
57
|
| `reserve [ios\|android]` | Lock a manually-started sim/emulator to the current project (no build) |
|
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import androidCommand from '../src/commands/android.js';
|
|
|
7
7
|
import startCommand from '../src/commands/start.js';
|
|
8
8
|
import stopCommand from '../src/commands/stop.js';
|
|
9
9
|
import logsCommand from '../src/commands/logs.js';
|
|
10
|
+
import pruneCommand from '../src/commands/prune.js';
|
|
10
11
|
import statusCommand from '../src/commands/status.js';
|
|
11
12
|
import releaseCommand from '../src/commands/release.js';
|
|
12
13
|
import reserveCommand from '../src/commands/reserve.js';
|
|
@@ -28,6 +29,7 @@ androidCommand(program);
|
|
|
28
29
|
startCommand(program);
|
|
29
30
|
stopCommand(program);
|
|
30
31
|
logsCommand(program);
|
|
32
|
+
pruneCommand(program);
|
|
31
33
|
statusCommand(program);
|
|
32
34
|
releaseCommand(program);
|
|
33
35
|
reserveCommand(program);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -77,7 +77,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
|
|
|
77
77
|
## When things go wrong
|
|
78
78
|
|
|
79
79
|
- **"No rn-iso assignment for project"** — run `npx rn-iso ios` (or android) first.
|
|
80
|
-
- **"All iOS simulators are claimed by other rn-iso projects"** (under `--auto`) — every existing sim is held by another project. Options: free another project (`npx rn-iso release` from there), pass `--device-type "iPhone 17 Pro"` to create a new sim, or re-run without `--auto` (in a real TTY) and ask the user before confirming the take-over prompt.
|
|
80
|
+
- **"All iOS simulators are claimed by other rn-iso projects"** (under `--auto`) — every existing sim is held by another project. Claims from deleted worktrees don't count (they're auto-reclaimed), so these are all live projects. Options: `npx rn-iso prune` if you suspect stale state, free another project (`npx rn-iso release` from there), pass `--device-type "iPhone 17 Pro"` to create a new sim, or re-run without `--auto` (in a real TTY) and ask the user before confirming the take-over prompt.
|
|
81
81
|
- **"All Android AVDs are claimed by other rn-iso projects"** — same situation on Android. Free another project or re-run interactively to take one over.
|
|
82
82
|
- **Wrong sim got the app** — older `@expo/cli` (< 54.0.24) had a bug where the launch ignored `--device`. Bump expo to 54.0.34+ if on SDK 54.
|
|
83
83
|
- **Blank screen / app installed but nothing renders** — check `npx rn-iso status`. Metro `stopped` almost always means the build ran WITHOUT `--managed-metro`, so Metro died with the shell that ran it: recover with `npx rn-iso start`, then relaunch the app (`xcrun simctl launch <UDID> <bundleId>`), and pass the flag next time. If Metro IS running, read `npx rn-iso logs -n 50` for bundle/resolution errors (a stale `node_modules` after a branch switch is a classic — reinstall deps, then `npx rn-iso stop` + `start`).
|
|
@@ -89,6 +89,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
|
|
|
89
89
|
|
|
90
90
|
- `npx rn-iso status` — show all projects, their assignments, and Metro state.
|
|
91
91
|
- `npx rn-iso logs [<port>|<shortcut>|<path>] [-n <lines>] [--follow]` — print the managed Metro log (default: last 50 lines of the current project's). This is where bundle progress, module-resolution errors, and client console logs land. **Check this first on a blank screen or red box** — it's faster than screenshots.
|
|
92
|
+
- `npx rn-iso prune` — remove entries for projects whose directory no longer exists (deleted worktrees), freeing their sims/emulators and ports, and killing any orphaned Metro. Live projects are never touched. Claims from deleted worktrees are also ignored automatically during device selection, so prune is housekeeping, not a prerequisite.
|
|
92
93
|
- `npx rn-iso start [--reset-cache] [-- <extras...>]` — start Metro detached on the project's assigned port WITHOUT building/installing. `--reset-cache` clears Metro's transform cache. Other extras after `--` are forwarded to `expo start` / `react-native start`.
|
|
93
94
|
- `npx rn-iso stop [<port>|<shortcut>|<path>]` — kill Metro. No arg = current project. Passing a port (e.g. `8083`) kills whatever is on it; a project shortcut (label or unique basename) or absolute path targets that project. Finds the process by port, so it works whether Metro was started by `npx rn-iso start` or by the build CLI.
|
|
94
95
|
- `npx rn-iso release [<port>|<shortcut>|<path>] [--platform <p>] [--shutdown]` — free a project's sim assignment. Defaults to the current project. Target can also be a Metro port (`8083`) or a shortcut (label / unique basename). `--shutdown` also stops the sim/emulator.
|
package/src/commands/android.js
CHANGED
|
@@ -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
|
|
64
|
-
// --no-
|
|
65
|
-
//
|
|
66
|
-
//
|
|
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);
|
package/src/commands/ios.js
CHANGED
|
@@ -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
|
|
60
|
-
// --no-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/commands/prune.js
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { pruneDeadProjects } from '../config.js';
|
|
4
|
+
import { findPidListeningOnPort } from '../metro.js';
|
|
5
|
+
|
|
6
|
+
export default function pruneCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('prune')
|
|
9
|
+
.description('Remove entries for projects whose directory no longer exists (deleted worktrees), freeing their sims/emulators and Metro ports. Live projects are never touched.')
|
|
10
|
+
.action(() => {
|
|
11
|
+
const removed = pruneDeadProjects();
|
|
12
|
+
if (removed.length === 0) {
|
|
13
|
+
console.log(chalk.dim('Nothing to prune: every registered project path still exists.'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const { path, project } of removed) {
|
|
18
|
+
const freed = [];
|
|
19
|
+
const ios = project.platforms?.ios;
|
|
20
|
+
if (ios?.deviceUdid) freed.push(`ios sim ${ios.deviceUdid}`);
|
|
21
|
+
const android = project.platforms?.android;
|
|
22
|
+
if (android?.avdName) freed.push(`android avd ${android.avdName}`);
|
|
23
|
+
else if (android?.serial) freed.push(`android device ${android.serial}`);
|
|
24
|
+
|
|
25
|
+
console.log(chalk.green(`Pruned ${path}`));
|
|
26
|
+
if (freed.length) console.log(chalk.dim(` freed: ${freed.join(', ')}`));
|
|
27
|
+
|
|
28
|
+
// A Metro started from the deleted directory can outlive it and squat
|
|
29
|
+
// on the port; kill it so the port is genuinely free.
|
|
30
|
+
if (typeof project.metroPort === 'number') {
|
|
31
|
+
const pid = findPidListeningOnPort(project.metroPort);
|
|
32
|
+
if (pid) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 'SIGTERM');
|
|
35
|
+
console.log(chalk.dim(` killed orphaned Metro pid ${pid} on port ${project.metroPort}`));
|
|
36
|
+
} catch {
|
|
37
|
+
console.log(chalk.dim(` could not kill pid ${pid} on port ${project.metroPort}`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk.dim(`\n${removed.length} project entr${removed.length === 1 ? 'y' : 'ies'} removed.`));
|
|
43
|
+
});
|
|
44
|
+
}
|
package/src/config.js
CHANGED
|
@@ -187,6 +187,10 @@ export function allClaimedDevices() {
|
|
|
187
187
|
};
|
|
188
188
|
if (!cfg) return result;
|
|
189
189
|
for (const [path, proj] of Object.entries(cfg.projects || {})) {
|
|
190
|
+
// Claims from project paths that no longer exist on disk are orphaned --
|
|
191
|
+
// nothing can ever run from a deleted worktree again -- so pickers treat
|
|
192
|
+
// those devices as free. `prune` removes the dead entries themselves.
|
|
193
|
+
if (!existsSync(path)) continue;
|
|
190
194
|
const label = path.split('/').pop() || path;
|
|
191
195
|
const ios = proj.platforms?.ios;
|
|
192
196
|
if (ios?.deviceUdid) {
|
|
@@ -218,6 +222,22 @@ export function allClaimedDevices() {
|
|
|
218
222
|
return result;
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
// Remove project entries whose path no longer exists on disk (deleted
|
|
226
|
+
// worktrees). Returns the removed entries so callers can report what was
|
|
227
|
+
// freed and clean up any process still bound to their Metro ports.
|
|
228
|
+
export function pruneDeadProjects() {
|
|
229
|
+
const cfg = loadConfig();
|
|
230
|
+
if (!cfg?.projects) return [];
|
|
231
|
+
const removed = [];
|
|
232
|
+
for (const [path, proj] of Object.entries(cfg.projects)) {
|
|
233
|
+
if (existsSync(path)) continue;
|
|
234
|
+
removed.push({ path, project: proj });
|
|
235
|
+
delete cfg.projects[path];
|
|
236
|
+
}
|
|
237
|
+
if (removed.length) saveConfig(cfg);
|
|
238
|
+
return removed;
|
|
239
|
+
}
|
|
240
|
+
|
|
221
241
|
export function recordSimUsage(platform, identifier) {
|
|
222
242
|
if (platform !== 'ios' && platform !== 'android') return;
|
|
223
243
|
const cfg = ensureConfig();
|
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
|
-
|
|
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/ports.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { request } from 'http';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
2
3
|
import { loadConfig, allMetroPorts, removeProject } from './config.js';
|
|
3
4
|
|
|
4
5
|
export function isMetroRunning(port) {
|
|
@@ -29,6 +30,10 @@ export async function findReclaimablePort(excludeProjectPath, probe = isMetroRun
|
|
|
29
30
|
const candidates = [];
|
|
30
31
|
for (const [path, proj] of Object.entries(cfg.projects)) {
|
|
31
32
|
if (path === excludeProjectPath) continue;
|
|
33
|
+
// Only projects whose path no longer exists are reclaimable: reclaiming
|
|
34
|
+
// removes the whole entry, and doing that to a live project would also
|
|
35
|
+
// drop its device claim out from under it.
|
|
36
|
+
if (existsSync(path)) continue;
|
|
32
37
|
if (typeof proj.metroPort === 'number') {
|
|
33
38
|
candidates.push({ port: proj.metroPort, ownerPath: path });
|
|
34
39
|
}
|
package/src/runner.js
CHANGED
|
@@ -76,11 +76,17 @@ export function detectScriptCli(scriptBody) {
|
|
|
76
76
|
return 'unknown';
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
// Metro (detached, log file, pid tracked)
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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}`;
|