@houwert/conductor 0.31.0 → 0.32.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/dist/commands/metro.js +137 -1
- package/dist/commands/start-device.js +18 -4
- package/dist/index.js +10 -1
- package/package.json +1 -1
- package/skills/conductor-metro-debugger/SKILL.md +15 -0
package/dist/commands/metro.js
CHANGED
|
@@ -3,12 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.HELP = void 0;
|
|
4
4
|
exports.metroStop = metroStop;
|
|
5
5
|
exports.metroReload = metroReload;
|
|
6
|
+
exports.normalizeMetroLocation = normalizeMetroLocation;
|
|
7
|
+
exports.metroUse = metroUse;
|
|
6
8
|
exports.HELP = ` metro stop [--port N] Stop the Metro bundler process on a port (default 8081)
|
|
7
|
-
metro reload [--port N] [--target N] Reload the JS bundle without restarting native
|
|
9
|
+
metro reload [--port N] [--target N] Reload the JS bundle without restarting native
|
|
10
|
+
metro use <port|host:port> [<appId>] Point an RN app at a Metro other than its compiled-in one
|
|
11
|
+
metro use --reset [<appId>] Drop the override, back to the compiled-in port`;
|
|
8
12
|
const child_process_1 = require("child_process");
|
|
9
13
|
const output_js_1 = require("../output.js");
|
|
10
14
|
const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
|
|
11
15
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
16
|
+
const runner_js_1 = require("../runner.js");
|
|
17
|
+
const session_js_1 = require("../session.js");
|
|
12
18
|
async function pidsOnPort(port) {
|
|
13
19
|
return new Promise((resolve) => {
|
|
14
20
|
const proc = (0, child_process_1.spawn)('lsof', ['-ti', `tcp:${port}`], {
|
|
@@ -109,3 +115,133 @@ async function metroReload(opts, sessionName, metroOpts) {
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* NSUserDefaults key RCTBundleURLProvider reads before falling back to the
|
|
120
|
+
* RCT_METRO_PORT baked into React-Core at pod-install time. Writing it is the
|
|
121
|
+
* only way to move a build onto another port without recompiling.
|
|
122
|
+
*/
|
|
123
|
+
const JS_LOCATION_KEY = 'RCT_jsLocation';
|
|
124
|
+
/**
|
|
125
|
+
* `8102` → `localhost:8102`. Also tolerates a pasted `http://host:port/`.
|
|
126
|
+
*
|
|
127
|
+
* Takes a number too: minimist turns a bare port argument into one.
|
|
128
|
+
*/
|
|
129
|
+
function normalizeMetroLocation(raw) {
|
|
130
|
+
const trimmed = String(raw)
|
|
131
|
+
.trim()
|
|
132
|
+
.replace(/^https?:\/\//, '')
|
|
133
|
+
.replace(/\/+$/, '');
|
|
134
|
+
const match = trimmed.match(/^(?:([A-Za-z0-9._-]+):)?(\d+)$/);
|
|
135
|
+
if (!match) {
|
|
136
|
+
throw new Error(`invalid location "${raw}" — expected <port> or <host:port>`);
|
|
137
|
+
}
|
|
138
|
+
const [, host = 'localhost', rawPort] = match;
|
|
139
|
+
const port = Number(rawPort);
|
|
140
|
+
if (port < 1 || port > 65535) {
|
|
141
|
+
throw new Error(`invalid port "${rawPort}" in "${raw}" — expected 1-65535`);
|
|
142
|
+
}
|
|
143
|
+
return `${host}:${port}`;
|
|
144
|
+
}
|
|
145
|
+
async function isPackagerRunning(location) {
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(`http://${location}/status`, {
|
|
148
|
+
signal: AbortSignal.timeout(2000),
|
|
149
|
+
});
|
|
150
|
+
return res.ok && (await res.text()).includes('packager-status:running');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Point a React Native app at a specific Metro by writing RCTBundleURLProvider's
|
|
158
|
+
* `RCT_jsLocation` default into the app's preferences on a simulator.
|
|
159
|
+
*
|
|
160
|
+
* The port an app asks for is compiled into React-Core from `RCT_METRO_PORT` at
|
|
161
|
+
* pod-install time, so a build made against the wrong port (a git worktree with
|
|
162
|
+
* its own Metro, a pod install from a shell missing the env var) keeps asking
|
|
163
|
+
* for the old one until it is recompiled. This override survives reinstalls and
|
|
164
|
+
* needs no rebuild.
|
|
165
|
+
*/
|
|
166
|
+
async function metroUse(opts, sessionName, useOpts) {
|
|
167
|
+
const deviceId = sessionName && sessionName !== 'default' ? sessionName : await (0, runner_js_1.detectFirstDevice)();
|
|
168
|
+
if (!deviceId) {
|
|
169
|
+
(0, output_js_1.printError)('metro use: no device found. Pass --device <id> or boot a simulator.', opts);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId).catch(() => undefined);
|
|
173
|
+
if (platform !== 'ios' && platform !== 'tvos') {
|
|
174
|
+
(0, output_js_1.printError)(`metro use: only iOS/tvOS simulators are supported (device is ${platform ?? 'unknown'}).\n` +
|
|
175
|
+
'On Android, map the port instead: adb -s <serial> reverse tcp:8081 tcp:<metro-port>', opts);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
const kind = await (0, bootstrap_js_1.detectDeviceKind)(deviceId).catch(() => undefined);
|
|
179
|
+
if (kind === 'physical') {
|
|
180
|
+
(0, output_js_1.printError)('metro use: physical devices read their packager host from the bundled ip.txt, ' +
|
|
181
|
+
'not from simulator defaults. Set it from the in-app dev menu instead.', opts);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
const appId = useOpts.appId ?? (await (0, session_js_1.getSession)(sessionName)).appId;
|
|
185
|
+
if (!appId) {
|
|
186
|
+
(0, output_js_1.printError)('metro use: no appId given and no active session. Run launch-app first.', opts);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
if (useOpts.reset) {
|
|
190
|
+
// `defaults delete` exits non-zero when the key was never written, which is
|
|
191
|
+
// the same end state the caller asked for.
|
|
192
|
+
await (0, runner_js_1.spawnCommand)('xcrun', [
|
|
193
|
+
'simctl',
|
|
194
|
+
'spawn',
|
|
195
|
+
deviceId,
|
|
196
|
+
'defaults',
|
|
197
|
+
'delete',
|
|
198
|
+
appId,
|
|
199
|
+
JS_LOCATION_KEY,
|
|
200
|
+
]);
|
|
201
|
+
if (opts.json)
|
|
202
|
+
(0, output_js_1.printData)({ appId, deviceId, location: null, reset: true }, opts);
|
|
203
|
+
else
|
|
204
|
+
(0, output_js_1.printSuccess)(`Cleared Metro override for "${appId}" — relaunch the app to apply`, opts);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
if (!useOpts.location) {
|
|
208
|
+
(0, output_js_1.printError)('Usage: conductor metro use <port|host:port> [<appId>] | metro use --reset', opts);
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
let location;
|
|
212
|
+
try {
|
|
213
|
+
location = normalizeMetroLocation(useOpts.location);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
(0, output_js_1.printError)(`metro use: ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
const running = await isPackagerRunning(location);
|
|
220
|
+
const result = await (0, runner_js_1.spawnCommand)('xcrun', [
|
|
221
|
+
'simctl',
|
|
222
|
+
'spawn',
|
|
223
|
+
deviceId,
|
|
224
|
+
'defaults',
|
|
225
|
+
'write',
|
|
226
|
+
appId,
|
|
227
|
+
JS_LOCATION_KEY,
|
|
228
|
+
location,
|
|
229
|
+
]);
|
|
230
|
+
if (!result.success) {
|
|
231
|
+
(0, output_js_1.printError)(`metro use: could not write ${JS_LOCATION_KEY} for "${appId}"\n${result.stderr}`, opts);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
if (!running) {
|
|
235
|
+
// RCTBundleURLProvider drops a stored location when nothing answers there,
|
|
236
|
+
// so the app would silently fall back to its compiled-in port.
|
|
237
|
+
process.stderr.write(`warning: no packager answered at ${location}; the app falls back to its ` +
|
|
238
|
+
'compiled-in port until Metro is up there.\n');
|
|
239
|
+
}
|
|
240
|
+
if (opts.json) {
|
|
241
|
+
(0, output_js_1.printData)({ appId, deviceId, location, packagerRunning: running, reset: false }, opts);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
(0, output_js_1.printSuccess)(`"${appId}" → ${location} — relaunch the app to apply`, opts);
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
@@ -35,6 +35,22 @@ const output_js_1 = require("../output.js");
|
|
|
35
35
|
const utils_js_1 = require("../utils.js");
|
|
36
36
|
const cli_js_1 = require("../drivers/vega/cli.js");
|
|
37
37
|
const discovery_js_1 = require("../drivers/roku/discovery.js");
|
|
38
|
+
/**
|
|
39
|
+
* Show the simulator window. Xcode 27 replaced Simulator.app with DeviceHub.app,
|
|
40
|
+
* so fall back to the hub when Simulator.app is absent.
|
|
41
|
+
*/
|
|
42
|
+
function openSimulatorUI() {
|
|
43
|
+
const tryOpen = (args, next) => {
|
|
44
|
+
const p = (0, child_process_1.spawn)('open', args, { detached: true, stdio: 'ignore' });
|
|
45
|
+
p.on('error', () => next?.());
|
|
46
|
+
p.on('exit', (code) => {
|
|
47
|
+
if (code !== 0)
|
|
48
|
+
next?.();
|
|
49
|
+
});
|
|
50
|
+
p.unref();
|
|
51
|
+
};
|
|
52
|
+
tryOpen(['-a', 'Simulator'], () => tryOpen(['-b', 'com.apple.dt.Devices']));
|
|
53
|
+
}
|
|
38
54
|
const IOS_BOOT_TIMEOUT_MS = 120000;
|
|
39
55
|
const ANDROID_BOOT_TIMEOUT_MS = 120000;
|
|
40
56
|
const POLL_MS = 1000;
|
|
@@ -252,8 +268,7 @@ async function startIOS(osVersion, opts, name, deviceType) {
|
|
|
252
268
|
return 1;
|
|
253
269
|
}
|
|
254
270
|
}
|
|
255
|
-
|
|
256
|
-
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
271
|
+
openSimulatorUI();
|
|
257
272
|
const displayName = name ?? device.name;
|
|
258
273
|
// Prewarm the driver so the first interaction command is not the
|
|
259
274
|
// one that pays the XCTest runner startup cost.
|
|
@@ -416,8 +431,7 @@ async function startTvOS(osVersion, opts, name, deviceType) {
|
|
|
416
431
|
return 1;
|
|
417
432
|
}
|
|
418
433
|
}
|
|
419
|
-
|
|
420
|
-
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
434
|
+
openSimulatorUI();
|
|
421
435
|
const displayName = name ?? device.name;
|
|
422
436
|
// Prewarm the driver so the first interaction command is not the
|
|
423
437
|
// one that pays the XCTest runner startup cost.
|
package/dist/index.js
CHANGED
|
@@ -229,6 +229,7 @@ async function main() {
|
|
|
229
229
|
'report',
|
|
230
230
|
'timeline',
|
|
231
231
|
'baselines',
|
|
232
|
+
'reset',
|
|
232
233
|
],
|
|
233
234
|
string: [
|
|
234
235
|
'device',
|
|
@@ -1219,8 +1220,16 @@ async function main() {
|
|
|
1219
1220
|
else if (sub === 'reload') {
|
|
1220
1221
|
exitCode = await (0, metro_js_1.metroReload)(opts, metroSession, { port, targetIndex });
|
|
1221
1222
|
}
|
|
1223
|
+
else if (sub === 'use') {
|
|
1224
|
+
exitCode = await (0, metro_js_1.metroUse)(opts, metroSession, {
|
|
1225
|
+
location: rest[1],
|
|
1226
|
+
appId: rest[2],
|
|
1227
|
+
reset: argv['reset'],
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1222
1230
|
else {
|
|
1223
|
-
console.error('Usage: conductor metro <stop|reload> [--port N] [--target N]'
|
|
1231
|
+
console.error('Usage: conductor metro <stop|reload|use> [--port N] [--target N]\n' +
|
|
1232
|
+
' conductor metro use <port|host:port> [<appId>] [--reset]');
|
|
1224
1233
|
exitCode = 1;
|
|
1225
1234
|
}
|
|
1226
1235
|
break;
|
package/package.json
CHANGED
|
@@ -54,6 +54,21 @@ running indefinitely.
|
|
|
54
54
|
|---|---|
|
|
55
55
|
| `conductor metro reload [--port N] [--target N]` | Reload the JS bundle without restarting native |
|
|
56
56
|
| `conductor metro stop [--port N]` | Stop the Metro bundler on a port (default 8081) |
|
|
57
|
+
| `conductor metro use <port\|host:port> [<appId>]` | Point the app at a specific Metro (iOS/tvOS simulators) |
|
|
58
|
+
| `conductor metro use --reset [<appId>]` | Drop that override, back to the compiled-in port |
|
|
59
|
+
|
|
60
|
+
An app asks for the Metro port compiled into React-Core from `RCT_METRO_PORT` at
|
|
61
|
+
pod-install time, so a build can end up asking for a port nothing is serving —
|
|
62
|
+
typically a git worktree whose Metro runs elsewhere, or a pod install from a
|
|
63
|
+
shell that lacked the env var. Symptom: the app never loads a bundle (or keeps
|
|
64
|
+
running a stale one) while `conductor metro reload` works fine against the port
|
|
65
|
+
you expect. `metro use` overrides it via the `RCT_jsLocation` preference, so no
|
|
66
|
+
recompile is needed and the setting survives reinstalls. **Relaunch the app**
|
|
67
|
+
afterwards — it is read when the bridge starts. The app id defaults to the
|
|
68
|
+
current session's.
|
|
69
|
+
|
|
70
|
+
Android has no such preference; map the port with
|
|
71
|
+
`adb -s <serial> reverse tcp:8081 tcp:<metro-port>` instead.
|
|
57
72
|
|
|
58
73
|
## Tips
|
|
59
74
|
|