@tamagui/native-ci 2.0.0-rc.8 → 2.0.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.
package/dist/metro.mjs CHANGED
@@ -9,15 +9,20 @@ async function waitForMetro(options) {
9
9
  console.info("Waiting for Metro to start...");
10
10
  for (let i = 1; i <= maxAttempts; i++) {
11
11
  try {
12
- if ((await fetch(`${METRO_URL}/`, {
12
+ const response = await fetch(`${METRO_URL}/`, {
13
13
  headers: {
14
14
  "Expo-Platform": platform
15
15
  }
16
- })).ok) return console.info("Metro is responding!"), !0;
16
+ });
17
+ if (response.ok) {
18
+ console.info("Metro is responding!");
19
+ return true;
20
+ }
17
21
  } catch {}
18
- console.info(`Waiting for Metro... (${i}/${maxAttempts})`), await Bun.sleep(intervalMs);
22
+ console.info(`Waiting for Metro... (${i}/${maxAttempts})`);
23
+ await Bun.sleep(intervalMs);
19
24
  }
20
- return !1;
25
+ return false;
21
26
  }
22
27
  async function prewarmBundle(platform) {
23
28
  console.info("Pre-warming bundle...");
@@ -28,19 +33,47 @@ async function prewarmBundle(platform) {
28
33
  }
29
34
  });
30
35
  if (response.ok) {
31
- const bundleUrl = (await response.json())?.launchAsset?.url;
32
- 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
+ }
33
45
  }
34
- } catch {
46
+ } catch (error) {
35
47
  console.info("Bundle pre-warm completed (with error, continuing)");
36
48
  }
37
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
+ }
38
64
  function startMetro() {
39
- console.info(`
40
- --- Starting Metro bundler ---`);
41
- const isCI = !!process.env.CI,
42
- args = ["bun", "expo", "start", "--dev-client", "--offline"];
43
- 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
+ }
44
77
  const proc = Bun.spawn(args, {
45
78
  env: {
46
79
  ...process.env,
@@ -53,32 +86,39 @@ function startMetro() {
53
86
  proc,
54
87
  kill: async () => {
55
88
  const pid = proc.pid;
56
- if (pid) try {
57
- await Bun.spawn(["pkill", "-P", String(pid)], {
58
- stdout: "ignore",
59
- stderr: "ignore"
60
- }).exited;
61
- } catch {}
62
- 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");
63
99
  }
64
100
  };
65
101
  }
66
102
  function setupSignalHandlers(metro) {
67
103
  const cleanup = async signal => {
68
104
  console.info(`
69
- 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);
70
108
  };
71
- process.on("SIGINT", () => cleanup("SIGINT")), process.on("SIGTERM", () => cleanup("SIGTERM"));
109
+ process.on("SIGINT", () => cleanup("SIGINT"));
110
+ process.on("SIGTERM", () => cleanup("SIGTERM"));
72
111
  }
73
112
  async function isMetroRunning(platform) {
74
113
  try {
75
- return (await fetch(`${METRO_URL}/`, {
114
+ const response = await fetch(`${METRO_URL}/`, {
76
115
  headers: {
77
116
  "Expo-Platform": platform
78
117
  }
79
- })).ok;
118
+ });
119
+ return response.ok;
80
120
  } catch {
81
- return !1;
121
+ return false;
82
122
  }
83
123
  }
84
124
  function killPortProcess() {
@@ -86,20 +126,31 @@ function killPortProcess() {
86
126
  const pid = execSync(`lsof -ti tcp:${METRO_PORT} -sTCP:LISTEN`, {
87
127
  encoding: "utf-8"
88
128
  }).trim();
89
- pid && (console.info(`Killing process ${pid} on port ${METRO_PORT}...`), process.kill(Number(pid), "SIGTERM"));
129
+ if (pid) {
130
+ console.info(`Killing process ${pid} on port ${METRO_PORT}...`);
131
+ process.kill(Number(pid), "SIGTERM");
132
+ }
90
133
  } catch {}
91
134
  }
92
135
  async function withMetro(platform, fn) {
93
- if (await isMetroRunning(platform)) return console.info(`
94
- --- 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
+ }
95
142
  killPortProcess();
96
143
  const metro = startMetro();
97
144
  setupSignalHandlers(metro);
98
145
  try {
99
- if (!(await waitForMetro({
146
+ const metroReady = await waitForMetro({
100
147
  platform
101
- }))) throw new Error("Metro failed to start within timeout");
102
- 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();
103
154
  } finally {
104
155
  await metro.kill();
105
156
  }
@@ -1 +1 @@
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","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","killPortProcess","encoding","trim","Number","withMetro","fn","Error"],"sources":["../src/metro.ts"],"sourcesContent":[null],"mappings":"AAQA,SAASA,QAAA,QAAgB;AACzB,SACEC,SAAA,EACAC,UAAA,EACAC,2BAAA,EACAC,8BAAA,QAGK;AAuBP,eAAsBC,aAAaC,OAAA,EAAyC;EAC1E,MAAM;IACJC,QAAA;IACAC,WAAA,GAAcL,2BAAA;IACdM,UAAA,GAAaL;EACf,IAAIE,OAAA;EAEJI,OAAA,CAAQC,IAAA,CAAK,+BAA+B;EAE5C,SAASC,CAAA,GAAI,GAAGA,CAAA,IAAKJ,WAAA,EAAaI,CAAA,IAAK;IACrC,IAAI;MAIF,KAHiB,MAAMC,KAAA,CAAM,GAAGZ,SAAS,KAAK;QAC5Ca,OAAA,EAAS;UAAE,iBAAiBP;QAAS;MACvC,CAAC,GACYQ,EAAA,EACX,OAAAL,OAAA,CAAQC,IAAA,CAAK,sBAAsB,GAC5B;IAEX,QAAQ,CAER;IACAD,OAAA,CAAQC,IAAA,CAAK,yBAAyBC,CAAC,IAAIJ,WAAW,GAAG,GACzD,MAAMQ,GAAA,CAAIC,KAAA,CAAMR,UAAU;EAC5B;EAEA,OAAO;AACT;AAMA,eAAsBS,cAAcX,QAAA,EAAmC;EACrEG,OAAA,CAAQC,IAAA,CAAK,uBAAuB;EAEpC,IAAI;IACF,MAAMQ,QAAA,GAAW,MAAMN,KAAA,CAAM,GAAGZ,SAAS,KAAK;MAC5Ca,OAAA,EAAS;QAAE,iBAAiBP;MAAS;IACvC,CAAC;IAED,IAAIY,QAAA,CAASJ,EAAA,EAAI;MAEf,MAAMK,SAAA,IADY,MAAMD,QAAA,CAASE,IAAA,CAAK,IACVC,WAAA,EAAaC,GAAA;MAErCH,SAAA,IACFV,OAAA,CAAQC,IAAA,CAAK,yBAAyBS,SAAS,EAAE,GACjD,MAAMP,KAAA,CAAMO,SAAS,GACrBV,OAAA,CAAQC,IAAA,CAAK,oBAAoB,KAEjCD,OAAA,CAAQC,IAAA,CAAK,oDAAoD;IAErE;EACF,QAAgB;IAEdD,OAAA,CAAQC,IAAA,CAAK,oDAAoD;EACnE;AACF;AAOO,SAASa,WAAA,EAA2B;EACzCd,OAAA,CAAQC,IAAA,CAAK;AAAA,+BAAkC;EAG/C,MAAMc,IAAA,GAAO,CAAC,CAACC,OAAA,CAAQC,GAAA,CAAIC,EAAA;IACrBC,IAAA,GAAO,CAAC,OAAO,QAAQ,SAAS,gBAAgB,WAAW;EAC7DJ,IAAA,KACFI,IAAA,CAAKC,IAAA,CAAK,SAAS,GACnBpB,OAAA,CAAQC,IAAA,CAAK,mCAAmC;EAGlD,MAAMoB,IAAA,GAAOf,GAAA,CAAIgB,KAAA,CAAMH,IAAA,EAAM;IAC3BF,GAAA,EAAK;MAAE,GAAGD,OAAA,CAAQC,GAAA;MAAKM,iBAAA,EAAmB;IAAO;IACjDC,MAAA,EAAQ;IACRC,MAAA,EAAQ;EACV,CAAC;EAED,OAAO;IACLJ,IAAA;IACAK,IAAA,EAAM,MAAAA,CAAA,KAAY;MAGhB,MAAMC,GAAA,GAAMN,IAAA,CAAKM,GAAA;MACjB,IAAIA,GAAA,EACF,IAAI;QAEF,MAAMrB,GAAA,CAAIgB,KAAA,CAAM,CAAC,SAAS,MAAMM,MAAA,CAAOD,GAAG,CAAC,GAAG;UAC5CH,MAAA,EAAQ;UACRC,MAAA,EAAQ;QACV,CAAC,EAAEI,MAAA;MACL,QAAQ,CAER;MAEFR,IAAA,CAAKK,IAAA,CAAK,SAAS,GACnB1B,OAAA,CAAQC,IAAA,CAAK,eAAe;IAC9B;EACF;AACF;AAMO,SAAS6B,oBAAoBC,KAAA,EAA2B;EAC7D,MAAMC,OAAA,GAAU,MAAOC,MAAA,IAAmB;IACxCjC,OAAA,CAAQC,IAAA,CAAK;AAAA,WAAcgC,MAAM,kBAAkB,GACnD,MAAMF,KAAA,CAAML,IAAA,CAAK,GACjBV,OAAA,CAAQkB,IAAA,CAAKD,MAAA,KAAW,WAAW,MAAM,GAAG;EAC9C;EAEAjB,OAAA,CAAQmB,EAAA,CAAG,UAAU,MAAMH,OAAA,CAAQ,QAAQ,CAAC,GAC5ChB,OAAA,CAAQmB,EAAA,CAAG,WAAW,MAAMH,OAAA,CAAQ,SAAS,CAAC;AAChD;AAKA,eAAeI,eAAevC,QAAA,EAAsC;EAClE,IAAI;IAIF,QAHiB,MAAMM,KAAA,CAAM,GAAGZ,SAAS,KAAK;MAC5Ca,OAAA,EAAS;QAAE,iBAAiBP;MAAS;IACvC,CAAC,GACeQ,EAAA;EAClB,QAAQ;IACN,OAAO;EACT;AACF;AAMA,SAASgC,gBAAA,EAAwB;EAC/B,IAAI;IACF,MAAMV,GAAA,GAAMrC,QAAA,CAAS,gBAAgBE,UAAU,iBAAiB;MAC9D8C,QAAA,EAAU;IACZ,CAAC,EAAEC,IAAA,CAAK;IACJZ,GAAA,KACF3B,OAAA,CAAQC,IAAA,CAAK,mBAAmB0B,GAAG,YAAYnC,UAAU,KAAK,GAC9DwB,OAAA,CAAQU,IAAA,CAAKc,MAAA,CAAOb,GAAG,GAAG,SAAS;EAEvC,QAAQ,CAER;AACF;AAUA,eAAsBc,UAAa5C,QAAA,EAAoB6C,EAAA,EAAkC;EAIvF,IAFuB,MAAMN,cAAA,CAAevC,QAAQ,GAGlD,OAAAG,OAAA,CAAQC,IAAA,CAAK;AAAA,yDAA4D,GACzE,MAAMO,aAAA,CAAcX,QAAQ,GACrB,MAAM6C,EAAA,CAAG;EAIlBL,eAAA,CAAgB;EAEhB,MAAMN,KAAA,GAAQjB,UAAA,CAAW;EACzBgB,mBAAA,CAAoBC,KAAK;EAEzB,IAAI;IAEF,IAAI,EADe,MAAMpC,YAAA,CAAa;MAAEE;IAAS,CAAC,IAEhD,MAAM,IAAI8C,KAAA,CAAM,sCAAsC;IAGxD,aAAMnC,aAAA,CAAcX,QAAQ,GAErB,MAAM6C,EAAA,CAAG;EAClB,UAAE;IACA,MAAMX,KAAA,CAAML,IAAA,CAAK;EACnB;AACF","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.8",
3
+ "version": "2.0.0",
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.8",
45
+ "@tamagui/build": "2.0.0",
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,11 +94,18 @@ 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
 
96
- // 45 min timeout for detox tests (in ms) - allows room within 60 min CI job timeout
97
- const DETOX_TIMEOUT_MS = 45 * 60 * 1000
105
+ // 60 min timeout for detox tests (in ms). the heaviest shard's real work is ~38min
106
+ // (cold metro bundle + per-test app reloads), so 45min left too little margin; the
107
+ // workflow job timeout is 90min, leaving room for setup + this run.
108
+ const DETOX_TIMEOUT_MS = 60 * 60 * 1000
98
109
 
99
110
  /**
100
111
  * Reset Detox lock file to prevent ECOMPROMISED errors in CI
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
package/src/metro.ts CHANGED
@@ -95,6 +95,26 @@ export async function prewarmBundle(platform: Platform): Promise<void> {
95
95
  }
96
96
  }
97
97
 
98
+ /**
99
+ * Detect if the current project uses expo-dev-client by checking package.json.
100
+ */
101
+ function projectUsesDevClient(): boolean {
102
+ try {
103
+ const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf-8'))
104
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
105
+ // check for expo-dev-client dep, or --dev-client in the start script
106
+ if (deps['expo-dev-client']) return true
107
+ if (
108
+ typeof pkg.scripts?.start === 'string' &&
109
+ pkg.scripts.start.includes('--dev-client')
110
+ )
111
+ return true
112
+ return false
113
+ } catch {
114
+ return false
115
+ }
116
+ }
117
+
98
118
  /**
99
119
  * Start Metro bundler as a background process.
100
120
  *
@@ -105,7 +125,12 @@ export function startMetro(): MetroProcess {
105
125
 
106
126
  // Only clear cache in CI - locally we want fast startup using cached transforms
107
127
  const isCI = !!process.env.CI
108
- const args = ['bun', 'expo', 'start', '--dev-client', '--offline']
128
+ const useDevClient = projectUsesDevClient()
129
+ const args = ['bun', 'expo', 'start', '--offline']
130
+ if (useDevClient) {
131
+ args.splice(3, 0, '--dev-client')
132
+ console.info('Dev client detected')
133
+ }
109
134
  if (isCI) {
110
135
  args.push('--clear')
111
136
  console.info('CI detected: clearing Metro cache')