@tamagui/native-ci 2.0.0-rc.4 → 2.0.0-rc.40

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/metro.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { METRO_URL, DEFAULT_METRO_WAIT_ATTEMPTS, DEFAULT_METRO_WAIT_INTERVAL_MS } from "./constants.mjs";
1
+ import { execSync } from "node:child_process";
2
+ import { METRO_URL, METRO_PORT, DEFAULT_METRO_WAIT_ATTEMPTS, DEFAULT_METRO_WAIT_INTERVAL_MS } from "./constants.mjs";
2
3
  async function waitForMetro(options) {
3
4
  const {
4
5
  platform,
@@ -8,15 +9,20 @@ async function waitForMetro(options) {
8
9
  console.info("Waiting for Metro to start...");
9
10
  for (let i = 1; i <= maxAttempts; i++) {
10
11
  try {
11
- if ((await fetch(`${METRO_URL}/`, {
12
+ const response = await fetch(`${METRO_URL}/`, {
12
13
  headers: {
13
14
  "Expo-Platform": platform
14
15
  }
15
- })).ok) return console.info("Metro is responding!"), !0;
16
+ });
17
+ if (response.ok) {
18
+ console.info("Metro is responding!");
19
+ return true;
20
+ }
16
21
  } catch {}
17
- console.info(`Waiting for Metro... (${i}/${maxAttempts})`), await Bun.sleep(intervalMs);
22
+ console.info(`Waiting for Metro... (${i}/${maxAttempts})`);
23
+ await Bun.sleep(intervalMs);
18
24
  }
19
- return !1;
25
+ return false;
20
26
  }
21
27
  async function prewarmBundle(platform) {
22
28
  console.info("Pre-warming bundle...");
@@ -27,19 +33,47 @@ async function prewarmBundle(platform) {
27
33
  }
28
34
  });
29
35
  if (response.ok) {
30
- const bundleUrl = (await response.json())?.launchAsset?.url;
31
- bundleUrl ? (console.info(`Fetching bundle from: ${bundleUrl}`), await fetch(bundleUrl), console.info("Bundle pre-warmed!")) : console.info("No bundle URL found in manifest, skipping pre-warm");
36
+ const manifest = await response.json();
37
+ const bundleUrl = manifest?.launchAsset?.url;
38
+ if (bundleUrl) {
39
+ console.info(`Fetching bundle from: ${bundleUrl}`);
40
+ await fetch(bundleUrl);
41
+ console.info("Bundle pre-warmed!");
42
+ } else {
43
+ console.info("No bundle URL found in manifest, skipping pre-warm");
44
+ }
32
45
  }
33
- } catch {
46
+ } catch (error) {
34
47
  console.info("Bundle pre-warm completed (with error, continuing)");
35
48
  }
36
49
  }
50
+ function projectUsesDevClient() {
51
+ try {
52
+ const pkg = JSON.parse(require("fs").readFileSync("package.json", "utf-8"));
53
+ const deps = {
54
+ ...pkg.dependencies,
55
+ ...pkg.devDependencies
56
+ };
57
+ if (deps["expo-dev-client"]) return true;
58
+ if (typeof pkg.scripts?.start === "string" && pkg.scripts.start.includes("--dev-client")) return true;
59
+ return false;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
37
64
  function startMetro() {
38
- console.info(`
39
- --- Starting Metro bundler ---`);
40
- const isCI = !!process.env.CI,
41
- args = ["bun", "expo", "start", "--dev-client", "--offline"];
42
- isCI && (args.push("--clear"), console.info("CI detected: clearing Metro cache"));
65
+ console.info("\n--- Starting Metro bundler ---");
66
+ const isCI = !!process.env.CI;
67
+ const useDevClient = projectUsesDevClient();
68
+ const args = ["bun", "expo", "start", "--offline"];
69
+ if (useDevClient) {
70
+ args.splice(3, 0, "--dev-client");
71
+ console.info("Dev client detected");
72
+ }
73
+ if (isCI) {
74
+ args.push("--clear");
75
+ console.info("CI detected: clearing Metro cache");
76
+ }
43
77
  const proc = Bun.spawn(args, {
44
78
  env: {
45
79
  ...process.env,
@@ -52,44 +86,71 @@ function startMetro() {
52
86
  proc,
53
87
  kill: async () => {
54
88
  const pid = proc.pid;
55
- if (pid) try {
56
- await Bun.spawn(["pkill", "-P", String(pid)], {
57
- stdout: "ignore",
58
- stderr: "ignore"
59
- }).exited;
60
- } catch {}
61
- proc.kill("SIGKILL"), console.info("Metro stopped");
89
+ if (pid) {
90
+ try {
91
+ await Bun.spawn(["pkill", "-P", String(pid)], {
92
+ stdout: "ignore",
93
+ stderr: "ignore"
94
+ }).exited;
95
+ } catch {}
96
+ }
97
+ proc.kill("SIGKILL");
98
+ console.info("Metro stopped");
62
99
  }
63
100
  };
64
101
  }
65
102
  function setupSignalHandlers(metro) {
66
103
  const cleanup = async signal => {
67
104
  console.info(`
68
- Received ${signal}, cleaning up...`), await metro.kill(), process.exit(signal === "SIGINT" ? 130 : 143);
105
+ Received ${signal}, cleaning up...`);
106
+ await metro.kill();
107
+ process.exit(signal === "SIGINT" ? 130 : 143);
69
108
  };
70
- process.on("SIGINT", () => cleanup("SIGINT")), process.on("SIGTERM", () => cleanup("SIGTERM"));
109
+ process.on("SIGINT", () => cleanup("SIGINT"));
110
+ process.on("SIGTERM", () => cleanup("SIGTERM"));
71
111
  }
72
112
  async function isMetroRunning(platform) {
73
113
  try {
74
- return (await fetch(`${METRO_URL}/`, {
114
+ const response = await fetch(`${METRO_URL}/`, {
75
115
  headers: {
76
116
  "Expo-Platform": platform
77
117
  }
78
- })).ok;
118
+ });
119
+ return response.ok;
79
120
  } catch {
80
- return !1;
121
+ return false;
81
122
  }
82
123
  }
124
+ function killPortProcess() {
125
+ try {
126
+ const pid = execSync(`lsof -ti tcp:${METRO_PORT} -sTCP:LISTEN`, {
127
+ encoding: "utf-8"
128
+ }).trim();
129
+ if (pid) {
130
+ console.info(`Killing process ${pid} on port ${METRO_PORT}...`);
131
+ process.kill(Number(pid), "SIGTERM");
132
+ }
133
+ } catch {}
134
+ }
83
135
  async function withMetro(platform, fn) {
84
- if (await isMetroRunning(platform)) return console.info(`
85
- --- Metro already running, reusing existing instance ---`), await prewarmBundle(platform), await fn();
136
+ const alreadyRunning = await isMetroRunning(platform);
137
+ if (alreadyRunning) {
138
+ console.info("\n--- Metro already running, reusing existing instance ---");
139
+ await prewarmBundle(platform);
140
+ return await fn();
141
+ }
142
+ killPortProcess();
86
143
  const metro = startMetro();
87
144
  setupSignalHandlers(metro);
88
145
  try {
89
- if (!(await waitForMetro({
146
+ const metroReady = await waitForMetro({
90
147
  platform
91
- }))) throw new Error("Metro failed to start within timeout");
92
- return await prewarmBundle(platform), await fn();
148
+ });
149
+ if (!metroReady) {
150
+ throw new Error("Metro failed to start within timeout");
151
+ }
152
+ await prewarmBundle(platform);
153
+ return await fn();
93
154
  } finally {
94
155
  await metro.kill();
95
156
  }
@@ -1 +1 @@
1
- {"version":3,"names":["METRO_URL","DEFAULT_METRO_WAIT_ATTEMPTS","DEFAULT_METRO_WAIT_INTERVAL_MS","waitForMetro","options","platform","maxAttempts","intervalMs","console","info","i","fetch","headers","ok","Bun","sleep","prewarmBundle","response","bundleUrl","json","launchAsset","url","startMetro","isCI","process","env","CI","args","push","proc","spawn","EXPO_NO_TELEMETRY","stdout","stderr","kill","pid","String","exited","setupSignalHandlers","metro","cleanup","signal","exit","on","isMetroRunning","withMetro","fn","Error"],"sources":["../src/metro.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["execSync","METRO_URL","METRO_PORT","DEFAULT_METRO_WAIT_ATTEMPTS","DEFAULT_METRO_WAIT_INTERVAL_MS","waitForMetro","options","platform","maxAttempts","intervalMs","console","info","i","response","fetch","headers","ok","Bun","sleep","prewarmBundle","manifest","json","bundleUrl","launchAsset","url","error","projectUsesDevClient","pkg","JSON","parse","require","readFileSync","deps","dependencies","devDependencies","scripts","start","includes","startMetro","isCI","process","env","CI","useDevClient","args","splice","push","proc","spawn","EXPO_NO_TELEMETRY","stdout","stderr","kill","pid","String","exited","setupSignalHandlers","metro","cleanup","signal","exit","on","isMetroRunning","killPortProcess","encoding","trim","Number","withMetro","fn","alreadyRunning","metroReady","Error"],"sources":["../src/metro.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
package/dist/runner.mjs CHANGED
@@ -4,14 +4,14 @@ import { createCacheKey, loadCache, saveCache } from "./cache.mjs";
4
4
  import { generateFingerprint } from "./fingerprint.mjs";
5
5
  async function runWithCache(options) {
6
6
  const {
7
- platform,
8
- buildCommand,
9
- outputPaths,
10
- projectRoot = process.cwd(),
11
- cachePrefix = "native-build",
12
- debug = !1
13
- } = options,
14
- log = debug ? console.log : () => {};
7
+ platform,
8
+ buildCommand,
9
+ outputPaths,
10
+ projectRoot = process.cwd(),
11
+ cachePrefix = "native-build",
12
+ debug = false
13
+ } = options;
14
+ const log = debug ? console.log : () => {};
15
15
  log(`Generating ${platform} fingerprint...`);
16
16
  const {
17
17
  hash: fingerprint
@@ -28,12 +28,15 @@ async function runWithCache(options) {
28
28
  });
29
29
  log(`Cache key: ${cacheKey}`);
30
30
  const cached = loadCache(cacheKey);
31
- if (cached && cached.fingerprint === fingerprint) return log("Cache hit! Skipping build."), {
32
- cacheHit: !0,
33
- fingerprint,
34
- cacheKey,
35
- outputPaths
36
- };
31
+ if (cached && cached.fingerprint === fingerprint) {
32
+ log("Cache hit! Skipping build.");
33
+ return {
34
+ cacheHit: true,
35
+ fingerprint,
36
+ cacheKey,
37
+ outputPaths
38
+ };
39
+ }
37
40
  log(`Running build: ${buildCommand}`);
38
41
  const execOptions = {
39
42
  cwd: projectRoot,
@@ -46,13 +49,14 @@ async function runWithCache(options) {
46
49
  throw new Error(`Build failed: ${err.message}${err.stderr ? `
47
50
  ${err.stderr}` : ""}`);
48
51
  }
49
- return saveCache(cacheKey, {
52
+ saveCache(cacheKey, {
50
53
  fingerprint,
51
54
  timestamp: (/* @__PURE__ */new Date()).toISOString(),
52
55
  platform,
53
56
  outputPaths
54
- }), {
55
- cacheHit: !1,
57
+ });
58
+ return {
59
+ cacheHit: false,
56
60
  fingerprint,
57
61
  cacheKey,
58
62
  outputPaths
@@ -60,8 +64,12 @@ ${err.stderr}` : ""}`);
60
64
  }
61
65
  function setGitHubOutput(name, value) {
62
66
  const outputFile = process.env.GITHUB_OUTPUT;
63
- outputFile ? appendFileSync(outputFile, `${name}=${value}
64
- `) : console.info(`[GitHub Output] ${name}=${value}`);
67
+ if (outputFile) {
68
+ appendFileSync(outputFile, `${name}=${value}
69
+ `);
70
+ } else {
71
+ console.info(`[GitHub Output] ${name}=${value}`);
72
+ }
65
73
  }
66
74
  function isGitHubActions() {
67
75
  return !!process.env.GITHUB_ACTIONS;
@@ -1 +1 @@
1
- {"version":3,"names":["execSync","appendFileSync","createCacheKey","loadCache","saveCache","generateFingerprint","runWithCache","options","platform","buildCommand","outputPaths","projectRoot","process","cwd","cachePrefix","debug","log","console","hash","fingerprint","cacheKey","prefix","cached","cacheHit","execOptions","stdio","error","err","Error","message","stderr","timestamp","Date","toISOString","setGitHubOutput","name","value","outputFile","env","GITHUB_OUTPUT","info","isGitHubActions","GITHUB_ACTIONS","isCI","CI"],"sources":["../src/runner.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,QAAA,QAAsC;AAC/C,SAASC,cAAA,QAAsB;AAC/B,SAASC,cAAA,EAAgBC,SAAA,EAAWC,SAAA,QAAiB;AACrD,SAASC,mBAAA,QAA2B;AAwBpC,eAAsBC,aACpBC,OAAA,EAC6B;EAC7B,MAAM;MACJC,QAAA;MACAC,YAAA;MACAC,WAAA;MACAC,WAAA,GAAcC,OAAA,CAAQC,GAAA,CAAI;MAC1BC,WAAA,GAAc;MACdC,KAAA,GAAQ;IACV,IAAIR,OAAA;IAEES,GAAA,GAAMD,KAAA,GAAQE,OAAA,CAAQD,GAAA,GAAM,MAAM,CAAC;EAGzCA,GAAA,CAAI,cAAcR,QAAQ,iBAAiB;EAC3C,MAAM;IAAEU,IAAA,EAAMC;EAAY,IAAI,MAAMd,mBAAA,CAAoB;IACtDG,QAAA;IACAG,WAAA;IACAI;EACF,CAAC;EACDC,GAAA,CAAI,gBAAgBG,WAAW,EAAE;EAGjC,MAAMC,QAAA,GAAWlB,cAAA,CAAe;IAAEM,QAAA;IAAUW,WAAA;IAAaE,MAAA,EAAQP;EAAY,CAAC;EAC9EE,GAAA,CAAI,cAAcI,QAAQ,EAAE;EAE5B,MAAME,MAAA,GAASnB,SAAA,CAAsDiB,QAAQ;EAE7E,IAAIE,MAAA,IAAUA,MAAA,CAAOH,WAAA,KAAgBA,WAAA,EACnC,OAAAH,GAAA,CAAI,4BAA4B,GACzB;IACLO,QAAA,EAAU;IACVJ,WAAA;IACAC,QAAA;IACAV;EACF;EAIFM,GAAA,CAAI,kBAAkBP,YAAY,EAAE;EACpC,MAAMe,WAAA,GAA+B;IACnCX,GAAA,EAAKF,WAAA;IACLc,KAAA,EAAOV,KAAA,GAAQ,YAAY,CAAC,QAAQ,QAAQ,MAAM;EACpD;EAEA,IAAI;IACFf,QAAA,CAASS,YAAA,EAAce,WAAW;EACpC,SAASE,KAAA,EAAO;IACd,MAAMC,GAAA,GAAMD,KAAA;IACZ,MAAM,IAAIE,KAAA,CAAM,iBAAiBD,GAAA,CAAIE,OAAO,GAAGF,GAAA,CAAIG,MAAA,GAAS;AAAA,EAAKH,GAAA,CAAIG,MAAM,KAAK,EAAE,EAAE;EACtF;EAGA,OAAA1B,SAAA,CAAUgB,QAAA,EAAU;IAClBD,WAAA;IACAY,SAAA,GAAW,mBAAIC,IAAA,CAAK,GAAEC,WAAA,CAAY;IAClCzB,QAAA;IACAE;EACF,CAAC,GAEM;IACLa,QAAA,EAAU;IACVJ,WAAA;IACAC,QAAA;IACAV;EACF;AACF;AAMO,SAASwB,gBAAgBC,IAAA,EAAcC,KAAA,EAAqB;EACjE,MAAMC,UAAA,GAAazB,OAAA,CAAQ0B,GAAA,CAAIC,aAAA;EAC3BF,UAAA,GACFpC,cAAA,CAAeoC,UAAA,EAAY,GAAGF,IAAI,IAAIC,KAAK;AAAA,CAAI,IAG/CnB,OAAA,CAAQuB,IAAA,CAAK,mBAAmBL,IAAI,IAAIC,KAAK,EAAE;AAEnD;AAKO,SAASK,gBAAA,EAA2B;EACzC,OAAO,CAAC,CAAC7B,OAAA,CAAQ0B,GAAA,CAAII,cAAA;AACvB;AAKO,SAASC,KAAA,EAAgB;EAC9B,OAAO,CAAC,CAAC/B,OAAA,CAAQ0B,GAAA,CAAIM,EAAA;AACvB","ignoreList":[]}
1
+ {"version":3,"names":["execSync","appendFileSync","createCacheKey","loadCache","saveCache","generateFingerprint","runWithCache","options","platform","buildCommand","outputPaths","projectRoot","process","cwd","cachePrefix","debug","log","console","hash","fingerprint","cacheKey","prefix","cached","cacheHit","execOptions","stdio","error","err","Error","message","stderr","timestamp","Date","toISOString","setGitHubOutput","name","value","outputFile","env","GITHUB_OUTPUT","info","isGitHubActions","GITHUB_ACTIONS","isCI","CI"],"sources":["../src/runner.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,QAAA,QAAsC;AAC/C,SAASC,cAAA,QAAsB;AAC/B,SAASC,cAAA,EAAgBC,SAAA,EAAWC,SAAA,QAAiB;AACrD,SAASC,mBAAA,QAA2B;AAwBpC,eAAsBC,aACpBC,OAAA,EAC6B;EAC7B,MAAM;IACJC,QAAA;IACAC,YAAA;IACAC,WAAA;IACAC,WAAA,GAAcC,OAAA,CAAQC,GAAA,CAAI;IAC1BC,WAAA,GAAc;IACdC,KAAA,GAAQ;EACV,IAAIR,OAAA;EAEJ,MAAMS,GAAA,GAAMD,KAAA,GAAQE,OAAA,CAAQD,GAAA,GAAM,MAAM,CAAC;EAGzCA,GAAA,CAAI,cAAcR,QAAQ,iBAAiB;EAC3C,MAAM;IAAEU,IAAA,EAAMC;EAAY,IAAI,MAAMd,mBAAA,CAAoB;IACtDG,QAAA;IACAG,WAAA;IACAI;EACF,CAAC;EACDC,GAAA,CAAI,gBAAgBG,WAAW,EAAE;EAGjC,MAAMC,QAAA,GAAWlB,cAAA,CAAe;IAAEM,QAAA;IAAUW,WAAA;IAAaE,MAAA,EAAQP;EAAY,CAAC;EAC9EE,GAAA,CAAI,cAAcI,QAAQ,EAAE;EAE5B,MAAME,MAAA,GAASnB,SAAA,CAAsDiB,QAAQ;EAE7E,IAAIE,MAAA,IAAUA,MAAA,CAAOH,WAAA,KAAgBA,WAAA,EAAa;IAChDH,GAAA,CAAI,4BAA4B;IAChC,OAAO;MACLO,QAAA,EAAU;MACVJ,WAAA;MACAC,QAAA;MACAV;IACF;EACF;EAGAM,GAAA,CAAI,kBAAkBP,YAAY,EAAE;EACpC,MAAMe,WAAA,GAA+B;IACnCX,GAAA,EAAKF,WAAA;IACLc,KAAA,EAAOV,KAAA,GAAQ,YAAY,CAAC,QAAQ,QAAQ,MAAM;EACpD;EAEA,IAAI;IACFf,QAAA,CAASS,YAAA,EAAce,WAAW;EACpC,SAASE,KAAA,EAAO;IACd,MAAMC,GAAA,GAAMD,KAAA;IACZ,MAAM,IAAIE,KAAA,CAAM,iBAAiBD,GAAA,CAAIE,OAAO,GAAGF,GAAA,CAAIG,MAAA,GAAS;AAAA,EAAKH,GAAA,CAAIG,MAAM,KAAK,EAAE,EAAE;EACtF;EAGA1B,SAAA,CAAUgB,QAAA,EAAU;IAClBD,WAAA;IACAY,SAAA,GAAW,mBAAIC,IAAA,CAAK,GAAEC,WAAA,CAAY;IAClCzB,QAAA;IACAE;EACF,CAAC;EAED,OAAO;IACLa,QAAA,EAAU;IACVJ,WAAA;IACAC,QAAA;IACAV;EACF;AACF;AAMO,SAASwB,gBAAgBC,IAAA,EAAcC,KAAA,EAAqB;EACjE,MAAMC,UAAA,GAAazB,OAAA,CAAQ0B,GAAA,CAAIC,aAAA;EAC/B,IAAIF,UAAA,EAAY;IACdpC,cAAA,CAAeoC,UAAA,EAAY,GAAGF,IAAI,IAAIC,KAAK;AAAA,CAAI;EACjD,OAAO;IAELnB,OAAA,CAAQuB,IAAA,CAAK,mBAAmBL,IAAI,IAAIC,KAAK,EAAE;EACjD;AACF;AAKO,SAASK,gBAAA,EAA2B;EACzC,OAAO,CAAC,CAAC7B,OAAA,CAAQ0B,GAAA,CAAII,cAAA;AACvB;AAKO,SAASC,KAAA,EAAgB;EAC9B,OAAO,CAAC,CAAC/B,OAAA,CAAQ0B,GAAA,CAAIM,EAAA;AACvB","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/native-ci",
3
- "version": "2.0.0-rc.4",
3
+ "version": "2.0.0-rc.40",
4
4
  "description": "Native CI/CD helpers for React Native apps with Expo - fingerprinting, caching, and build optimization",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  "./package.json": "./package.json",
25
25
  ".": {
26
26
  "types": "./types/index.d.ts",
27
+ "browser": "./dist/index.mjs",
27
28
  "module": "./dist/index.mjs",
28
29
  "import": "./dist/index.mjs"
29
30
  }
@@ -38,10 +39,10 @@
38
39
  "clean:build": "tamagui-build clean:build"
39
40
  },
40
41
  "dependencies": {
41
- "@expo/fingerprint": "^0.15.3"
42
+ "@expo/fingerprint": "~0.16.6"
42
43
  },
43
44
  "devDependencies": {
44
- "@tamagui/build": "2.0.0-rc.4",
45
+ "@tamagui/build": "2.0.0-rc.40",
45
46
  "@types/bun": "^1.1.0",
46
47
  "@types/node": "^22.1.0"
47
48
  },
package/src/cli.ts CHANGED
@@ -26,7 +26,14 @@ import {
26
26
  } from './deps'
27
27
  import { withMetro } from './metro'
28
28
  import { parseDetoxArgs, runDetoxTests } from './detox'
29
- import { ensureIOSFolder, ensureIOSApp } from './ios'
29
+ import {
30
+ ensureIOSFolder,
31
+ ensureIOSApp,
32
+ ensureBootedSimulator,
33
+ ensureAppInstalled,
34
+ getBootedSimulatorUDID,
35
+ getMaestroBundleId,
36
+ } from './ios'
30
37
  import { setupAndroidDevice, ensureAndroidFolder } from './android'
31
38
  import type { Platform } from './constants'
32
39
 
@@ -196,6 +203,8 @@ try {
196
203
  const platform = subcommand || 'ios'
197
204
 
198
205
  if (platform === 'ios') {
206
+ ensureBootedSimulator()
207
+
199
208
  // Ensure iOS dependencies
200
209
  await ensureIosDeps()
201
210
 
@@ -244,6 +253,8 @@ try {
244
253
  })
245
254
  process.exit(exitCode)
246
255
  } else if (platform === 'maestro') {
256
+ ensureBootedSimulator()
257
+
247
258
  // Ensure Maestro is installed
248
259
  await ensureMaestro()
249
260
 
@@ -254,17 +265,28 @@ try {
254
265
 
255
266
  process.chdir(options.projectRoot)
256
267
 
268
+ // resolve the target simulator UDID once and use it consistently
269
+ const udid = getBootedSimulatorUDID()!
270
+
271
+ // ensure the app is built and installed on the target simulator
272
+ const bundleId = getMaestroBundleId(options.projectRoot)
273
+ await ensureAppInstalled({ projectRoot: options.projectRoot, bundleId, udid })
274
+
257
275
  // Run Maestro with Metro for development builds
258
276
  const exitCode = await withMetro('ios', async () => {
259
277
  const { $ } = await import('bun')
260
278
  // Flows are at ./flows/ in kitchen-sink, not .maestro/flows/
261
279
  const flowArg = flow ? `./flows/${flow}` : './flows'
280
+ const udidArgs = ['--udid', udid]
281
+ const debugDir = `${options.projectRoot}/.maestro-debug`
262
282
  const result =
263
- await $`maestro test ${flowArg} --exclude-tags=util --no-ansi`.nothrow()
283
+ await $`maestro test ${flowArg} --exclude-tags=util --no-ansi --debug-output=${debugDir} ${udidArgs}`.nothrow()
264
284
  return result.exitCode
265
285
  })
266
286
  process.exit(exitCode)
267
287
  } else if (platform === 'all') {
288
+ ensureBootedSimulator()
289
+
268
290
  console.info('=== Running All Native Tests ===\n')
269
291
 
270
292
  // Run iOS tests
package/src/detox.ts CHANGED
@@ -21,6 +21,8 @@ export interface DetoxRunnerOptions {
21
21
  headless?: boolean
22
22
  /** Number of parallel workers (default: 1) */
23
23
  workers?: number
24
+ /** Specific test files to run (passed as positional args to detox) */
25
+ testFiles?: string[]
24
26
  }
25
27
 
26
28
  /**
@@ -29,7 +31,7 @@ export interface DetoxRunnerOptions {
29
31
  export function parseDetoxArgs(platform: Platform) {
30
32
  const defaultConfig = platform === 'ios' ? 'ios.sim.debug' : 'android.emu.ci.debug'
31
33
 
32
- const { values } = parseArgs({
34
+ const { values, positionals } = parseArgs({
33
35
  options: {
34
36
  config: { type: 'string', default: defaultConfig },
35
37
  'project-root': { type: 'string', default: process.cwd() },
@@ -38,6 +40,7 @@ export function parseDetoxArgs(platform: Platform) {
38
40
  retries: { type: 'string', default: '0' },
39
41
  workers: { type: 'string', default: '1' },
40
42
  },
43
+ allowPositionals: true,
41
44
  })
42
45
 
43
46
  // Validate and convert retries to number
@@ -61,6 +64,7 @@ export function parseDetoxArgs(platform: Platform) {
61
64
  recordLogs: values['record-logs']!,
62
65
  retries: retriesNum,
63
66
  workers: workersNum,
67
+ testFiles: positionals.length > 0 ? positionals : undefined,
64
68
  }
65
69
  }
66
70
 
@@ -90,6 +94,11 @@ export function buildDetoxArgs(options: DetoxRunnerOptions): string[] {
90
94
  args.push('--workers', String(options.workers))
91
95
  }
92
96
 
97
+ // append specific test files if provided (for CI sharding)
98
+ if (options.testFiles && options.testFiles.length > 0) {
99
+ args.push(...options.testFiles)
100
+ }
101
+
93
102
  return args
94
103
  }
95
104
 
package/src/ios.ts CHANGED
@@ -9,6 +9,187 @@ import { $ } from 'bun'
9
9
  import { isCI } from './runner'
10
10
  import { generateFingerprint } from './fingerprint'
11
11
 
12
+ /**
13
+ * Check if any iOS simulator is booted and available for testing.
14
+ * Returns true if at least one simulator is booted.
15
+ */
16
+ export function hasBootedSimulator(): boolean {
17
+ return !!getBootedSimulatorUDID()
18
+ }
19
+
20
+ /**
21
+ * Get the UDID of the first booted iOS simulator, or null if none.
22
+ */
23
+ export function getBootedSimulatorUDID(): string | null {
24
+ try {
25
+ const output = execSync('xcrun simctl list devices booted', {
26
+ encoding: 'utf-8',
27
+ stdio: ['pipe', 'pipe', 'pipe'],
28
+ })
29
+ const match = output.match(/\(([A-F0-9-]{36})\)/i)
30
+ return match ? match[1] : null
31
+ } catch {
32
+ return null
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Boot an iOS simulator. Picks the first available iPhone device.
38
+ */
39
+ export function ensureBootedSimulator(): void {
40
+ if (hasBootedSimulator()) return
41
+
42
+ console.info('No booted iOS simulator found, booting one...')
43
+ try {
44
+ const output = execSync('xcrun simctl list devices available', {
45
+ encoding: 'utf-8',
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ })
48
+ // find first available iPhone
49
+ const match = output.match(/iPhone[^\n]*\(([A-F0-9-]{36})\)/i)
50
+ if (!match) {
51
+ throw new Error('No available iPhone simulator found. Install one via Xcode.')
52
+ }
53
+ const udid = match[1]
54
+ console.info(`Booting simulator ${udid}...`)
55
+ execSync(`xcrun simctl boot ${udid}`, { stdio: 'inherit' })
56
+ console.info('Simulator booted.')
57
+ } catch (err) {
58
+ throw new Error(
59
+ `Failed to boot iOS simulator: ${err instanceof Error ? err.message : err}`
60
+ )
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Check if an app is installed on the booted simulator.
66
+ */
67
+ function isAppInstalled(bundleId: string, udid: string): boolean {
68
+ try {
69
+ execSync(`xcrun simctl get_app_container "${udid}" "${bundleId}"`, {
70
+ stdio: ['pipe', 'pipe', 'pipe'],
71
+ })
72
+ return true
73
+ } catch {
74
+ return false
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Ensure the app is installed on a specific simulator.
80
+ * For dev client apps, builds if needed then installs.
81
+ * For Expo Go apps, ensures Expo Go is present.
82
+ */
83
+ export async function ensureAppInstalled(opts: {
84
+ projectRoot: string
85
+ bundleId: string
86
+ udid?: string
87
+ }): Promise<void> {
88
+ const { projectRoot, bundleId } = opts
89
+ const udid = opts.udid || getBootedSimulatorUDID() || 'booted'
90
+
91
+ // check if app is already installed
92
+ if (isAppInstalled(bundleId, udid)) {
93
+ console.info(`App ${bundleId} already installed on simulator ${udid}`)
94
+ return
95
+ }
96
+
97
+ if (bundleId === 'host.exp.Exponent') {
98
+ await installExpoGo(udid)
99
+ return
100
+ }
101
+
102
+ // custom dev client - build and install
103
+ console.info(`App ${bundleId} not installed, building...`)
104
+ await ensureIOSFolder()
105
+
106
+ const appPath =
107
+ process.env.DETOX_IOS_APP_PATH ||
108
+ 'ios/build/Build/Products/Debug-iphonesimulator/tamaguikitchensink.app'
109
+ const fullAppPath = join(projectRoot, appPath)
110
+
111
+ if (!existsSync(fullAppPath)) {
112
+ await ensureIOSApp('ios.sim.debug')
113
+ }
114
+
115
+ console.info(`Installing app on simulator ${udid}...`)
116
+ execSync(`xcrun simctl install "${udid}" "${fullAppPath}"`, { stdio: 'inherit' })
117
+ console.info('App installed.')
118
+ }
119
+
120
+ /**
121
+ * Get the bundle ID needed for maestro tests.
122
+ * Checks flow files first (they declare appId), falls back to app.json.
123
+ */
124
+ export function getMaestroBundleId(projectRoot: string): string {
125
+ // check flow files for appId
126
+ const flowsDir = join(projectRoot, 'flows')
127
+ if (existsSync(flowsDir)) {
128
+ try {
129
+ const files = require('fs').readdirSync(flowsDir) as string[]
130
+ for (const f of files) {
131
+ if (!f.endsWith('.yaml')) continue
132
+ const content = readFileSync(join(flowsDir, f), 'utf-8')
133
+ const match = content.match(/^appId:\s*(.+)$/m)
134
+ if (match) return match[1].trim()
135
+ }
136
+ } catch {}
137
+ }
138
+
139
+ // fall back to app.json
140
+ try {
141
+ const appJson = JSON.parse(readFileSync(join(projectRoot, 'app.json'), 'utf-8'))
142
+ return appJson.expo?.ios?.bundleIdentifier || 'host.exp.Exponent'
143
+ } catch {
144
+ return 'host.exp.Exponent'
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Download and install Expo Go on the booted simulator.
150
+ */
151
+ async function installExpoGo(udid: string): Promise<void> {
152
+ const tmpDir = join(require('os').tmpdir(), 'expo-go-install')
153
+ const appPath = join(tmpDir, 'Expo Go.app')
154
+
155
+ // skip download if already cached
156
+ if (existsSync(appPath)) {
157
+ console.info(`Installing cached Expo Go on simulator ${udid}...`)
158
+ execSync(`xcrun simctl install "${udid}" "${appPath}"`, { stdio: 'inherit' })
159
+ console.info('Expo Go installed.')
160
+ return
161
+ }
162
+
163
+ console.info('Downloading Expo Go...')
164
+ mkdirSync(tmpDir, { recursive: true })
165
+
166
+ // get the download URL from expo's version API
167
+ const { getVersionsAsync } =
168
+ // @ts-ignore - no type declarations for internal expo module
169
+ require('@expo/cli/build/src/api/getVersions') as {
170
+ getVersionsAsync: () => Promise<any>
171
+ }
172
+ const versions = await getVersionsAsync()
173
+
174
+ // find the latest SDK version that has an iOS client URL
175
+ const sdkVersions = Object.entries(versions.sdkVersions || {})
176
+ .filter(([, v]: [string, any]) => v.iosClientUrl)
177
+ .sort(([a], [b]) => b.localeCompare(a, undefined, { numeric: true }))
178
+ const clientUrl = (sdkVersions[0]?.[1] as any)?.iosClientUrl
179
+
180
+ if (!clientUrl) {
181
+ throw new Error('Could not find Expo Go download URL')
182
+ }
183
+
184
+ execSync(`curl -fsSL "${clientUrl}" -o "${tmpDir}/ExpoGo.tar.gz"`, { stdio: 'inherit' })
185
+ // archive contains .app contents directly, extract into .app bundle
186
+ mkdirSync(appPath, { recursive: true })
187
+ execSync(`tar -xzf "${tmpDir}/ExpoGo.tar.gz" -C "${appPath}"`, { stdio: 'inherit' })
188
+
189
+ execSync(`xcrun simctl install "${udid}" "${appPath}"`, { stdio: 'inherit' })
190
+ console.info('Expo Go installed.')
191
+ }
192
+
12
193
  /**
13
194
  * Shutdown all simulators and clean up zombie simulator processes.
14
195
  * macOS doesn't properly clean up simulators between test runs, leading to
@@ -107,6 +288,7 @@ function saveFingerprintCache(projectRoot: string, cache: FingerprintCache): voi
107
288
  *
108
289
  * IMPORTANT: Fingerprint only changes when NATIVE dependencies change
109
290
  * (Podfile, native modules, etc). JS-only changes don't require rebuild.
291
+ * Set SKIP_IOS_REBUILD=1 to skip rebuild even when fingerprint changes.
110
292
  */
111
293
  export async function ensureIOSApp(config: string = 'ios.sim.debug'): Promise<void> {
112
294
  // On CI, the app is built separately - don't build here
@@ -153,29 +335,24 @@ export async function ensureIOSApp(config: string = 'ios.sim.debug'): Promise<vo
153
335
  return
154
336
  }
155
337
 
156
- // Fingerprint changed - but let user decide if they want to rebuild
157
- // Many fingerprint changes are false positives (timestamp changes, etc)
338
+ // fingerprint changed - rebuild by default since native deps likely changed
158
339
  if (cache?.fingerprint) {
159
340
  console.info(`\n--- iOS fingerprint changed ---`)
160
341
  console.info(`Previous: ${cache.fingerprint.slice(0, 16)}...`)
161
342
  console.info(`Current: ${currentFingerprint.slice(0, 16)}...`)
162
- console.info(`App exists at ${appPath}`)
163
343
 
164
- // Check if FORCE_IOS_REBUILD is set
165
- if (process.env.FORCE_IOS_REBUILD) {
166
- console.info('FORCE_IOS_REBUILD set, rebuilding...')
167
- await buildIOSApp(config, projectRoot, appPath, currentFingerprint)
344
+ if (process.env.SKIP_IOS_REBUILD) {
345
+ console.info('SKIP_IOS_REBUILD set, using existing app')
346
+ saveFingerprintCache(projectRoot, {
347
+ fingerprint: currentFingerprint,
348
+ timestamp: new Date().toISOString(),
349
+ appPath,
350
+ })
168
351
  return
169
352
  }
170
353
 
171
- // Otherwise use existing app but update fingerprint cache
172
- // This avoids unnecessary rebuilds from timestamp/metadata changes
173
- console.info('Using existing app (set FORCE_IOS_REBUILD=1 to force rebuild)')
174
- saveFingerprintCache(projectRoot, {
175
- fingerprint: currentFingerprint,
176
- timestamp: new Date().toISOString(),
177
- appPath,
178
- })
354
+ console.info('Native dependencies changed, rebuilding...')
355
+ await buildIOSApp(config, projectRoot, appPath, currentFingerprint)
179
356
  return
180
357
  }
181
358