@tamagui/native-ci 2.0.0-rc.9 → 2.1.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/cache.mjs +26 -13
- package/dist/cache.mjs.map +1 -1
- package/dist/cli.mjs +268 -117
- package/dist/cli.mjs.map +1 -1
- package/dist/constants.mjs +8 -8
- package/dist/constants.mjs.map +1 -1
- package/dist/deps.mjs +46 -19
- package/dist/deps.mjs.map +1 -1
- package/dist/detox.mjs +85 -56
- package/dist/detox.mjs.map +1 -1
- package/dist/fingerprint.mjs +13 -9
- package/dist/fingerprint.mjs.map +1 -1
- package/dist/index.js +9 -95
- package/dist/index.js.map +1 -6
- package/dist/metro.mjs +81 -30
- package/dist/metro.mjs.map +1 -1
- package/dist/runner.mjs +27 -19
- package/dist/runner.mjs.map +1 -1
- package/package.json +4 -3
- package/src/cli.ts +24 -2
- package/src/detox.ts +14 -3
- package/src/ios.ts +181 -0
- package/src/metro.ts +26 -1
- package/src/run-detox-android.ts +1 -0
- package/src/run-detox-ios.ts +1 -0
- package/types/detox.d.ts +3 -0
- package/types/detox.d.ts.map +1 -1
- package/types/ios.d.ts +28 -0
- package/types/ios.d.ts.map +1 -1
- package/types/metro.d.ts.map +1 -1
- package/dist/cache.js +0 -71
- package/dist/cache.js.map +0 -6
- package/dist/cli.js +0 -298
- package/dist/cli.js.map +0 -6
- package/dist/constants.js +0 -12
- package/dist/constants.js.map +0 -6
- package/dist/deps.js +0 -44
- package/dist/deps.js.map +0 -6
- package/dist/detox.js +0 -73
- package/dist/detox.js.map +0 -6
- package/dist/fingerprint.js +0 -43
- package/dist/fingerprint.js.map +0 -6
- package/dist/metro.js +0 -114
- package/dist/metro.js.map +0 -6
- package/dist/runner.js +0 -73
- package/dist/runner.js.map +0 -6
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
|
-
|
|
12
|
+
const response = await fetch(`${METRO_URL}/`, {
|
|
13
13
|
headers: {
|
|
14
14
|
"Expo-Platform": platform
|
|
15
15
|
}
|
|
16
|
-
})
|
|
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})`)
|
|
22
|
+
console.info(`Waiting for Metro... (${i}/${maxAttempts})`);
|
|
23
|
+
await Bun.sleep(intervalMs);
|
|
19
24
|
}
|
|
20
|
-
return
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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...`)
|
|
105
|
+
Received ${signal}, cleaning up...`);
|
|
106
|
+
await metro.kill();
|
|
107
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
70
108
|
};
|
|
71
|
-
process.on("SIGINT", () => cleanup("SIGINT"))
|
|
109
|
+
process.on("SIGINT", () => cleanup("SIGINT"));
|
|
110
|
+
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
72
111
|
}
|
|
73
112
|
async function isMetroRunning(platform) {
|
|
74
113
|
try {
|
|
75
|
-
|
|
114
|
+
const response = await fetch(`${METRO_URL}/`, {
|
|
76
115
|
headers: {
|
|
77
116
|
"Expo-Platform": platform
|
|
78
117
|
}
|
|
79
|
-
})
|
|
118
|
+
});
|
|
119
|
+
return response.ok;
|
|
80
120
|
} catch {
|
|
81
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
146
|
+
const metroReady = await waitForMetro({
|
|
100
147
|
platform
|
|
101
|
-
})
|
|
102
|
-
|
|
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
|
}
|
package/dist/metro.mjs.map
CHANGED
|
@@ -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","
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
52
|
+
saveCache(cacheKey, {
|
|
50
53
|
fingerprint,
|
|
51
54
|
timestamp: (/* @__PURE__ */new Date()).toISOString(),
|
|
52
55
|
platform,
|
|
53
56
|
outputPaths
|
|
54
|
-
})
|
|
55
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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;
|
package/dist/runner.mjs.map
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "2.1.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": "
|
|
42
|
+
"@expo/fingerprint": "~0.16.6"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@tamagui/build": "2.
|
|
45
|
+
"@tamagui/build": "2.1.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 {
|
|
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
|
-
//
|
|
97
|
-
|
|
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
|
|
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')
|