stim 1.8.0 → 1.9.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.
Files changed (80) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
  3. package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
  4. package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
  5. package/dist/android-cas-compiler.mjs +2 -2
  6. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
  7. package/dist/app-install-Uf3a83Tv.mjs +819 -0
  8. package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
  9. package/dist/build-progress-CVqkfPC6.mjs +188 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +2 -0
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
  21. package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
  22. package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
  27. package/dist/exec-jIAlgilk.mjs +132 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
  29. package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
  30. package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
  31. package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
  32. package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
  33. package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
  34. package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
  35. package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
  36. package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
  37. package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
  38. package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
  39. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
  40. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
  41. package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
  42. package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
  43. package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
  44. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  45. package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
  46. package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
  47. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
  48. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
  49. package/dist/server-expo-CeILEu86.mjs +2 -0
  50. package/dist/settings-BKNh5rl4.mjs +376 -0
  51. package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
  52. package/dist/settings.schema.json +989 -0
  53. package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
  54. package/dist/simslim-B_LsGLuJ.mjs +259 -0
  55. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
  56. package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
  57. package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
  58. package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
  59. package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
  60. package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
  61. package/dist/status-S6ehL4Ui.mjs +932 -0
  62. package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
  63. package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
  64. package/dist/supervisor-run.mjs +52 -34
  65. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
  66. package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
  67. package/dist/worktree-DKhSF0UX.mjs +2 -0
  68. package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
  69. package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
  70. package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
  71. package/package.json +5 -6
  72. package/skill/SKILL.md +2 -0
  73. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  74. package/dist/build-slots-C-oAHRT1.mjs +0 -151
  75. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  76. package/dist/exec-B29XNuhP.mjs +0 -77
  77. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  78. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  79. package/dist/status-DdkLki47.mjs +0 -426
  80. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -1,797 +0,0 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { E as ownership_claim_exports } from "./config-B7iyKWkL.mjs";
3
- import { c as ensureWorkspaceStorage } from "./workspace-state-BT5E3SA2.mjs";
4
- import { T as removeTemporaryEntry, w as makeTemporaryDirectory } from "./worktree-FG_ZKKdb.mjs";
5
- import { i as detectIsExpo, o as isPackageResolvable } from "./project-DDZWf_16.mjs";
6
- import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Dl5uKE03.mjs";
7
- import { t as NOT_OURS_FOREIGN_CWD } from "./metro-C7bcJ0uo.mjs";
8
- import { i as releaseBuildLock, n as acquireBuildLock } from "./build-lock-CgtsB6TV.mjs";
9
- import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-BVAA1OqJ.mjs";
10
- import { S as resolveEasCliBin } from "./teardown-BJqEzPo_.mjs";
11
- import { i as LAUNCH_BUNDLING, s as LAUNCH_UNVERIFIED } from "./app-install-BDbsSgCl.mjs";
12
- import { d as startBuildHeartbeat, f as tailLines, n as HEARTBEAT_INTERVAL_MS } from "./xcode-B6YaWeg0.mjs";
13
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
14
- import { createHash } from "node:crypto";
15
- import { basename, join } from "node:path";
16
- import chalk from "chalk";
17
- import { request } from "node:http";
18
- //#region src/engine/eas-build.ts
19
- function isEasBuildFailure(result) {
20
- return result?.ok === false;
21
- }
22
- function easOptionRefusal({ profile, isExpo, buildSelector, buildCache }) {
23
- if (profile === void 0) return null;
24
- if (profile.trim() && isExpo && buildSelector === void 0 && buildCache !== false) return null;
25
- return {
26
- code: "STIM_BAD_ARG",
27
- message: "--eas-profile requires an Expo project and a non-empty development profile.",
28
- remedy: "Use --eas-profile <name> without --scheme, --configuration, --variant, or --no-build-cache."
29
- };
30
- }
31
- function object(value) {
32
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
33
- }
34
- function shellArg(value) {
35
- return /^[a-zA-Z0-9_.-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
36
- }
37
- function easDeviceBuildRemedy(profile) {
38
- return `If the device is not registered, run npx eas-cli device:create. With approval for cloud build costs and signing changes, run npx eas-cli build --platform ios --profile ${shellArg(profile)} to produce a build with a valid profile for this device, then retry the same Stim command.`;
39
- }
40
- var EasFailure = class extends Error {
41
- code;
42
- remedy;
43
- constructor(code, message, remedy) {
44
- super(message);
45
- this.code = code;
46
- this.remedy = remedy;
47
- }
48
- };
49
- function selectEasBuild(builds, { platform, profile, fingerprint, projectId, physical = false }) {
50
- if (!Array.isArray(builds)) throw new Error("EAS build:list did not return an array.");
51
- for (const value of builds) {
52
- const build = object(value);
53
- if (typeof build.id === "string" && build.id.length > 0 && build.status === "FINISHED" && build.platform === platform.toUpperCase() && build.buildProfile === profile && object(build.fingerprint).hash === fingerprint && (object(build.app).id ?? object(build.project).id) === projectId && (platform !== "ios" || build.isForIosSimulator === !physical) && build.distribution === "INTERNAL" && typeof object(build.artifacts).applicationArchiveUrl === "string") return build.id;
54
- }
55
- return null;
56
- }
57
- async function resolveEasDevelopmentBuild({ root, platform, profile, note, isExpo = true, physical = false, selectors = [], buildCache }) {
58
- if (profile === void 0) return null;
59
- const refusal = easOptionRefusal({
60
- profile,
61
- isExpo,
62
- buildSelector: selectors.find((value) => value != null) ?? void 0,
63
- buildCache
64
- });
65
- if (refusal) return {
66
- ok: false,
67
- ...refusal
68
- };
69
- const retry = "the same Stim command";
70
- const buildCommand = `npx eas-cli build --platform ${platform} --profile ${shellArg(profile)}`;
71
- let lock = null;
72
- try {
73
- const cli = resolveEasCliBin(root);
74
- if (!cli) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS CLI is not installed.", "Install eas-cli, run eas login, then retry.");
75
- let profileEnv = {};
76
- const run = (args, timeoutMs = 12e4) => {
77
- try {
78
- return JSON.parse(getExecutor().runFile(cli.file, [
79
- ...args,
80
- "--json",
81
- "--non-interactive"
82
- ], {
83
- cwd: root,
84
- timeoutMs,
85
- env: profileEnv
86
- }));
87
- } catch {
88
- throw new EasFailure("STIM_EAS_UNAVAILABLE", `EAS ${args[0]} failed or returned invalid JSON.`, `Run npx eas-cli ${args.map(shellArg).join(" ")} to inspect the error, then retry ${retry}.`);
89
- }
90
- };
91
- note(`eas resolving profile ${profile}`);
92
- const config = object(run([
93
- "config",
94
- "--platform",
95
- platform,
96
- "--profile",
97
- profile
98
- ]));
99
- const buildProfile = object(config.buildProfile);
100
- profileEnv = object(buildProfile.env);
101
- const projectId = object(object(object(config.appConfig).extra).eas).projectId;
102
- if (buildProfile.developmentClient !== true || buildProfile.distribution !== "internal" || platform === "ios" && buildProfile.simulator === true !== !physical) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} is not an internal development build for this ${platform} target.`, `Set developmentClient: true and distribution: internal${platform === "ios" ? `, with ios.simulator: ${!physical}` : ""} in that eas.json profile.`);
103
- if (typeof projectId !== "string" || !projectId) throw new EasFailure("STIM_BAD_ARG", "The Expo app is not linked to an EAS project.", "Link the intended EAS project with eas init, then retry.");
104
- if (platform === "ios" ? buildProfile.buildConfiguration !== void 0 && buildProfile.buildConfiguration !== "Debug" : buildProfile.gradleCommand !== void 0 && (typeof buildProfile.gradleCommand !== "string" || !/^:app:assemble\w*Debug$/.test(buildProfile.gradleCommand))) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} overrides the development build configuration.`, "Use ios.buildConfiguration: Debug or an android.gradleCommand that assembles a single Debug APK, or remove the override.");
105
- note(`eas fingerprinting ${platform} with profile ${profile}`);
106
- const fingerprint = object(run([
107
- "fingerprint:generate",
108
- "--platform",
109
- platform,
110
- "--build-profile",
111
- profile
112
- ])).hash;
113
- if (typeof fingerprint !== "string" || !/^[a-f0-9]{40,64}$/.test(fingerprint)) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS returned no valid native fingerprint.", `Run npx eas-cli fingerprint:generate --platform ${platform} --build-profile ${shellArg(profile)} to inspect the result.`);
114
- note(`eas looking for ${fingerprint.slice(0, 8)}`);
115
- const buildId = selectEasBuild(run([
116
- "build:list",
117
- "--platform",
118
- platform,
119
- "--build-profile",
120
- profile,
121
- "--fingerprint-hash",
122
- fingerprint,
123
- "--status",
124
- "finished",
125
- ...platform === "ios" && !physical ? ["--simulator"] : ["--distribution", "internal"],
126
- "--limit",
127
- "1"
128
- ]), {
129
- platform,
130
- profile,
131
- fingerprint,
132
- projectId,
133
- physical
134
- });
135
- if (!buildId) throw new EasFailure("STIM_EAS_BUILD_MISSING", `No compatible EAS ${platform} build exists for profile ${profile} and fingerprint ${fingerprint}.`, `A cloud build may incur charges. With approval to run it, use ${buildCommand}, then retry ${retry}.`);
136
- const cacheKey = `eas-${createHash("sha256").update(JSON.stringify({
137
- projectId,
138
- buildId
139
- })).digest("hex")}-debug-${physical && platform === "ios" ? "device" : "sim"}`;
140
- lock = acquireBuildLock({
141
- platform,
142
- key: cacheKey,
143
- root
144
- });
145
- if (!lock.acquired) throw new EasFailure("STIM_EAS_UNAVAILABLE", `Another run holds the EAS artifact claim at ${lock.path}.`, `Wait for that run to finish, then retry ${retry}.`);
146
- note(`eas resolving artifact for build ${buildId}`);
147
- const result = object(run([
148
- "build:download",
149
- "--build-id",
150
- buildId
151
- ], 6e5));
152
- if (typeof result.path !== "string" || !existsSync(result.path)) throw new Error("EAS returned no downloaded artifact.");
153
- const path = realpathSync(result.path);
154
- if (platform === "ios" ? !path.endsWith(".app") || !statSync(path).isDirectory() : !path.endsWith(".apk") || !statSync(path).isFile()) throw new Error("EAS returned an unexpected artifact path or type.");
155
- note(`eas artifact ready ${fingerprint.slice(0, 8)}`);
156
- return {
157
- ok: true,
158
- path,
159
- fingerprint,
160
- cacheKey,
161
- cacheHit: "remote"
162
- };
163
- } catch (error) {
164
- const claim = (0, ownership_claim_exports.claimFailure)(error, null);
165
- if (claim) return {
166
- ok: false,
167
- code: claim.code,
168
- message: claim.message,
169
- remedy: claim.remedy
170
- };
171
- if (error instanceof EasFailure) return {
172
- ok: false,
173
- code: error.code,
174
- message: error.message,
175
- remedy: error.remedy
176
- };
177
- return {
178
- ok: false,
179
- code: "STIM_EAS_UNAVAILABLE",
180
- message: `Could not resolve the EAS development build: ${error.message}`,
181
- remedy: `Retry ${retry}. No cloud build was started.`
182
- };
183
- } finally {
184
- releaseBuildLock(lock);
185
- }
186
- }
187
- //#endregion
188
- //#region src/engine/metro-warmup.ts
189
- const WARMUP_TIMEOUT_MS = 6e4;
190
- async function requestLocal(url, platform, prefetch) {
191
- return new Promise((resolve) => {
192
- const req = request(url, { headers: {
193
- accept: "application/json",
194
- "expo-platform": platform,
195
- ...prefetch ? { "x-stim-metro-warmup": "1" } : {}
196
- } });
197
- const timer = setTimeout(() => req.destroy(), WARMUP_TIMEOUT_MS);
198
- timer.unref();
199
- req.on("socket", (socket) => socket.unref());
200
- req.on("error", () => resolve(null));
201
- req.on("close", () => {
202
- clearTimeout(timer);
203
- resolve(null);
204
- });
205
- req.on("response", (res) => {
206
- let body = "";
207
- res.setEncoding("utf8");
208
- res.on("data", (chunk) => {
209
- if (!prefetch) body += chunk;
210
- });
211
- res.on("error", () => resolve(null));
212
- res.on("end", () => resolve(res.statusCode === 200 ? body : null));
213
- });
214
- req.end();
215
- });
216
- }
217
- async function warmMetro({ port, platform, isExpo, appId, bundleUrl }) {
218
- try {
219
- const origin = `http://127.0.0.1:${port}`;
220
- if (await requestLocal(new URL("/_stim/metro-warmup", origin), platform, false) !== "ready") return;
221
- const url = new URL("/index.bundle", origin);
222
- url.search = new URLSearchParams({
223
- platform,
224
- dev: "true",
225
- lazy: "true",
226
- minify: "false",
227
- ...platform === "ios" ? { inlineSourceMap: "false" } : {},
228
- modulesOnly: "false",
229
- runModule: "true",
230
- excludeSource: "true",
231
- sourcePaths: "url-server",
232
- ...appId ? { app: appId } : {}
233
- }).toString();
234
- if (!bundleUrl && isExpo) {
235
- const body = await requestLocal(new URL(origin), platform, false);
236
- if (body === null) return;
237
- const manifest = JSON.parse(body);
238
- bundleUrl = manifest.launchAsset?.url ?? manifest.bundleUrl;
239
- if (typeof bundleUrl !== "string") return;
240
- }
241
- if (bundleUrl) {
242
- const bundle = new URL(bundleUrl, origin);
243
- url.pathname = bundle.pathname;
244
- url.search = bundle.search;
245
- }
246
- await requestLocal(url, platform, true);
247
- } catch {}
248
- }
249
- //#endregion
250
- //#region src/commands/native-runtime.ts
251
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
252
- const GATE_RETRY_DELAYS_MS = [
253
- 3e3,
254
- 7e3,
255
- 1e4
256
- ];
257
- function gateShouldRetry(resolution) {
258
- if (resolution?.metro) return false;
259
- return resolution?.kind !== NOT_OURS_FOREIGN_CWD;
260
- }
261
- async function resolveMetroWithRetry(resolve, port, root, { delays = GATE_RETRY_DELAYS_MS, sleep: wait = sleep, onRetry = (_info) => {} } = {}) {
262
- let resolution = await resolve(port, root);
263
- for (let i = 0; i < delays.length && gateShouldRetry(resolution); i++) {
264
- const delayMs = delays[i];
265
- if (delayMs === void 0) break;
266
- onRetry({
267
- attempt: i + 1,
268
- delayMs,
269
- resolution
270
- });
271
- await wait(delayMs);
272
- resolution = await resolve(port, root);
273
- }
274
- return resolution;
275
- }
276
- function noMetroMessage({ port, resolution, supervisor, supervisorAlive }) {
277
- const foreign = resolution?.notOurs;
278
- if (supervisor && supervisor.port === port && supervisorAlive) {
279
- const mode = supervisor.mode ? `${supervisor.mode} ` : "";
280
- return `A supervisor record exists for port ${port} (pid ${supervisor.pid}, ${mode}dev server) but it did not verify as this workspace's Metro${foreign ? `: ${foreign}` : " -- nothing answered /status"}. Metro may still be indexing this project (a monorepo file-map crawl blocks its event loop for ~20s after the port opens).`;
281
- }
282
- if (foreign) return `Port ${port} is in use but is NOT this workspace's dev server: ${foreign}.`;
283
- return `Nothing is serving this workspace's dev server on port ${port}.`;
284
- }
285
- function noMetroRemedy({ port, supervisor, supervisorAlive }) {
286
- if (supervisor && supervisor.port === port && supervisorAlive) return "Re-run `stim ios` in a few seconds, or give the dev server longer to verify with `stim start --wait <seconds>`.";
287
- return "Run `stim start` first, or pass --no-metro-check.";
288
- }
289
- async function ensureWorkspaceStorageSafely(root, { note = (_line) => {} } = {}) {
290
- try {
291
- return ensureWorkspaceStorage(root);
292
- } catch (err) {
293
- note(chalk.dim(`Could not prepare this workspace's Stim state: ${err?.message || err}`));
294
- throw err;
295
- }
296
- }
297
- function launchOutcomeRecord({ launchState, release, bundleId, configuration, metroPort }) {
298
- const unverified = launchState === LAUNCH_UNVERIFIED;
299
- const bundling = launchState === LAUNCH_BUNDLING;
300
- let msg;
301
- if (release) msg = unverified ? `${bundleId} could not be verified as running after its ${configuration} launch` : `${bundleId} is running its embedded ${configuration} bundle`;
302
- else if (unverified) msg = `no bundle request from ${bundleId} reached this workspace's Metro on port ${metroPort}`;
303
- else if (bundling) msg = `${bundleId} requested a bundle from this workspace's Metro on port ${metroPort}; it was still being built when the launch check ended`;
304
- else msg = `${bundleId} fetched a bundle from this workspace's Metro on port ${metroPort}`;
305
- return {
306
- src: "build",
307
- level: unverified ? "warn" : "info",
308
- event: unverified ? "launch_unverified" : bundling ? "launch_bundling" : "launch_verified",
309
- msg
310
- };
311
- }
312
- function isPhysicalDeviceRequest(flag) {
313
- return flag !== null && flag !== void 0 && flag !== false;
314
- }
315
- //#endregion
316
- //#region src/engine/prebuild.ts
317
- const PREBUILD_ERROR = "STIM_PREBUILD_FAILED";
318
- const LAST_LINES$1 = 20;
319
- function nativeDirName(platform) {
320
- return platform === "android" ? "android" : "ios";
321
- }
322
- function nativeDir(root, platform) {
323
- return join(root, nativeDirName(platform));
324
- }
325
- function shouldPrebuild({ isExpo, nativeDirExists }) {
326
- return Boolean(isExpo) && !nativeDirExists;
327
- }
328
- function needsPrebuild(root, platform, isExpo) {
329
- return shouldPrebuild({
330
- isExpo,
331
- nativeDirExists: existsSync(nativeDir(root, platform))
332
- });
333
- }
334
- function prebuildRefusal({ isExpo, platform, nativeDirExists }) {
335
- if (nativeDirExists || isExpo) return null;
336
- const dir = nativeDirName(platform);
337
- return {
338
- code: PREBUILD_ERROR,
339
- message: `This project has no ${dir}/ directory and is not an Expo (CNG) project, so Stim cannot generate one.`,
340
- remedy: `Check out or generate the native project (\`npx @react-native-community/cli init\` produced one originally), or add \`expo\` to the project so \`expo prebuild\` can create ${dir}/.`
341
- };
342
- }
343
- function prebuildFailure(logWriter, result) {
344
- logWriter?.write?.({
345
- src: "build",
346
- level: "error",
347
- msg: result.reason || "Expo prebuild failed.",
348
- event: "prebuild_failed"
349
- });
350
- return result;
351
- }
352
- async function runPrebuild(root, platform, logWriter, { spawnFn = null, now = Date.now, isExpo = null } = {}) {
353
- const dirExists = existsSync(nativeDir(root, platform));
354
- const refusal = prebuildRefusal({
355
- isExpo: isExpo === null ? detectIsExpo(root) : isExpo,
356
- platform,
357
- nativeDirExists: dirExists
358
- });
359
- if (refusal) return prebuildFailure(logWriter, {
360
- failed: true,
361
- code: refusal.code,
362
- reason: refusal.message,
363
- remedy: refusal.remedy,
364
- error: refusal,
365
- lastLines: []
366
- });
367
- const bin = expoBinPath(root);
368
- if (!bin) {
369
- const binRefusal = expoBinRefusal(root, "prebuild");
370
- return prebuildFailure(logWriter, {
371
- failed: true,
372
- code: PREBUILD_ERROR,
373
- reason: binRefusal.message,
374
- remedy: binRefusal.remedy,
375
- lastLines: []
376
- });
377
- }
378
- const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
379
- const startedAt = now();
380
- const tail = [];
381
- const push = (line) => {
382
- const msg = stripAnsi(String(line)).trimEnd();
383
- if (!msg.trim()) return;
384
- tail.push(msg);
385
- if (tail.length > LAST_LINES$1) tail.shift();
386
- logWriter?.write?.({
387
- src: "build",
388
- level: "debug",
389
- msg,
390
- raw: true,
391
- event: "prebuild"
392
- });
393
- };
394
- let child;
395
- try {
396
- child = spawn(bin, [
397
- "prebuild",
398
- "-p",
399
- platform,
400
- "--no-install"
401
- ], {
402
- cwd: root,
403
- stdio: [
404
- "ignore",
405
- "pipe",
406
- "pipe"
407
- ],
408
- env: {
409
- ...process.env,
410
- FORCE_COLOR: "0",
411
- CI: "1"
412
- }
413
- });
414
- } catch (err) {
415
- return prebuildFailure(logWriter, {
416
- failed: true,
417
- code: PREBUILD_ERROR,
418
- reason: `Could not run \`expo prebuild\`: ${err?.message || err}`,
419
- lastLines: []
420
- });
421
- }
422
- const outReader = createLineReader(push);
423
- const errReader = createLineReader(push);
424
- child.stdout?.setEncoding?.("utf-8");
425
- child.stderr?.setEncoding?.("utf-8");
426
- child.stdout?.on("data", (chunk) => outReader.push(chunk));
427
- child.stderr?.on("data", (chunk) => errReader.push(chunk));
428
- const result = await waitForChild(child);
429
- outReader.flush();
430
- errReader.flush();
431
- const durationMs = now() - startedAt;
432
- if (result.error) return prebuildFailure(logWriter, {
433
- failed: true,
434
- code: PREBUILD_ERROR,
435
- reason: `Could not run \`expo prebuild\`: ${result.error?.message || result.error}`,
436
- lastLines: tail.slice(),
437
- durationMs
438
- });
439
- if (result.code !== 0) {
440
- const how = result.signal ? `signal ${result.signal}` : `exit code ${result.code}`;
441
- return prebuildFailure(logWriter, {
442
- failed: true,
443
- code: PREBUILD_ERROR,
444
- reason: `\`expo prebuild -p ${platform}\` failed (${how}).`,
445
- lastLines: tail.slice(),
446
- durationMs
447
- });
448
- }
449
- if (!existsSync(nativeDir(root, platform))) return prebuildFailure(logWriter, {
450
- failed: true,
451
- code: PREBUILD_ERROR,
452
- reason: `\`expo prebuild -p ${platform}\` succeeded but did not create ${nativeDirName(platform)}/.`,
453
- lastLines: tail.slice(),
454
- durationMs
455
- });
456
- return {
457
- ok: true,
458
- durationMs,
459
- nativeDir: nativeDir(root, platform)
460
- };
461
- }
462
- //#endregion
463
- //#region src/engine/js-swap.ts
464
- const JS_BUNDLE_NAME = "main.jsbundle";
465
- const LAST_LINES = 5;
466
- function hermesEnabledFromProperties(text) {
467
- if (typeof text !== "string") return true;
468
- let data;
469
- try {
470
- data = JSON.parse(text);
471
- } catch {
472
- return true;
473
- }
474
- if (!data || typeof data !== "object") return true;
475
- const raw = data.hermesEnabled;
476
- return raw !== "false" && raw !== false;
477
- }
478
- function readHermesEnabled(root) {
479
- try {
480
- return hermesEnabledFromProperties(readFileSync(join(root, "ios", "Podfile.properties.json"), "utf-8"));
481
- } catch {
482
- return true;
483
- }
484
- }
485
- const ENTRY_CANDIDATES = [
486
- "index.js",
487
- "index.ts",
488
- "index.tsx",
489
- "index.jsx"
490
- ];
491
- function pickEntryFile(entries) {
492
- const names = new Set((Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string"));
493
- return ENTRY_CANDIDATES.find((c) => names.has(c)) ?? "index.js";
494
- }
495
- function detectEntryFile(root) {
496
- let entries;
497
- try {
498
- entries = readdirSync(root);
499
- } catch {
500
- return "index.js";
501
- }
502
- return pickEntryFile(entries);
503
- }
504
- function bundleCommand({ isExpo, entryFile, bundleOutput, assetsDest }) {
505
- if (isExpo) return {
506
- file: "npx",
507
- args: [
508
- "expo",
509
- "export:embed",
510
- "--platform",
511
- "ios",
512
- "--dev",
513
- "false",
514
- "--bundle-output",
515
- bundleOutput,
516
- "--assets-dest",
517
- assetsDest
518
- ]
519
- };
520
- return {
521
- file: "npx",
522
- args: [
523
- "react-native",
524
- "bundle",
525
- "--platform",
526
- "ios",
527
- "--dev",
528
- "false",
529
- "--entry-file",
530
- entryFile,
531
- "--bundle-output",
532
- bundleOutput,
533
- "--assets-dest",
534
- assetsDest
535
- ]
536
- };
537
- }
538
- function hermescPath(root, { exists = existsSync } = {}) {
539
- const candidates = [
540
- join(root, "node_modules", "hermes-compiler", "hermesc", "osx-bin", "hermesc"),
541
- join(root, "ios", "Pods", "hermes-engine", "destroot", "bin", "hermesc"),
542
- join(root, "node_modules", "react-native", "sdks", "hermesc", "osx-bin", "hermesc")
543
- ];
544
- return candidates.find(exists) ?? candidates[candidates.length - 1];
545
- }
546
- function hermescArgs({ bundle, out }) {
547
- return [
548
- "-emit-binary",
549
- "-out",
550
- out,
551
- bundle
552
- ];
553
- }
554
- async function swapJsBundle({ root, isExpo, cachedAppPath, logWriter = null, exec = null, spawnFn = null, mkdtemp = () => makeTemporaryDirectory(cachedAppPath, "stim-js-swap-"), exists = existsSync, hermesEnabled = null, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, onHeartbeat = (line) => console.error(line) }) {
555
- const e = exec || getExecutor();
556
- const startedAt = now();
557
- const elapsed = () => now() - startedAt;
558
- let tmp;
559
- const fail = (step, reason, lastLines = []) => {
560
- logWriter?.write?.({
561
- src: "build",
562
- level: "error",
563
- msg: `JS swap failed at ${step}: ${reason}`,
564
- event: "js_swap"
565
- });
566
- if (tmp) removeTemporaryEntry(tmp);
567
- return {
568
- failed: true,
569
- step,
570
- reason,
571
- lastLines,
572
- durationMs: elapsed()
573
- };
574
- };
575
- let appCopy;
576
- try {
577
- tmp = mkdtemp();
578
- appCopy = join(tmp, basename(cachedAppPath));
579
- try {
580
- e.runFile("cp", [
581
- "-c",
582
- "-R",
583
- cachedAppPath,
584
- appCopy
585
- ]);
586
- } catch {
587
- e.runFile("cp", [
588
- "-R",
589
- cachedAppPath,
590
- appCopy
591
- ]);
592
- }
593
- } catch (err) {
594
- return fail("copy", `could not copy ${cachedAppPath} aside: ${describe(err)}`);
595
- }
596
- const bundleOutput = join(tmp, JS_BUNDLE_NAME);
597
- const assetsDest = join(tmp, "assets");
598
- const command = bundleCommand({
599
- isExpo,
600
- entryFile: isExpo ? "index.js" : detectEntryFile(root),
601
- bundleOutput,
602
- assetsDest
603
- });
604
- try {
605
- mkdirSync(assetsDest, { recursive: true });
606
- } catch (err) {
607
- return fail("bundle", `could not create ${assetsDest}: ${describe(err)}`);
608
- }
609
- logWriter?.write?.({
610
- src: "build",
611
- level: "info",
612
- msg: `${command.file} ${command.args.join(" ")}`,
613
- event: "js_swap"
614
- });
615
- const spawn = spawnFn || ((cmd, args, opts) => e.spawn(cmd, args, opts));
616
- const transcript = [];
617
- const push = (line) => {
618
- const msg = cleanLine(line);
619
- if (msg.trim() === "") return;
620
- transcript.push(msg);
621
- logWriter?.write?.({
622
- src: "build",
623
- level: "debug",
624
- msg,
625
- raw: true,
626
- event: "js_swap"
627
- });
628
- };
629
- let child;
630
- try {
631
- child = spawn(command.file, command.args, {
632
- cwd: root,
633
- stdio: [
634
- "ignore",
635
- "pipe",
636
- "pipe"
637
- ],
638
- env: {
639
- ...process.env,
640
- FORCE_COLOR: "0"
641
- }
642
- });
643
- } catch (err) {
644
- return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(err)}`);
645
- }
646
- const reader = {
647
- out: createLineReader(push),
648
- err: createLineReader(push)
649
- };
650
- child.stdout?.setEncoding?.("utf-8");
651
- child.stderr?.setEncoding?.("utf-8");
652
- child.stdout?.on("data", (chunk) => reader.out.push(chunk));
653
- child.stderr?.on("data", (chunk) => reader.err.push(chunk));
654
- const stopHeartbeat = startBuildHeartbeat({
655
- intervalMs: heartbeatMs,
656
- elapsed,
657
- emit: onHeartbeat,
658
- label: "swap"
659
- });
660
- let outcome;
661
- try {
662
- outcome = await waitForChild(child);
663
- } finally {
664
- stopHeartbeat();
665
- }
666
- reader.out.flush();
667
- reader.err.flush();
668
- if (outcome.error) return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(outcome.error)}`);
669
- if (outcome.code !== 0) {
670
- const how = outcome.signal ? `signal ${outcome.signal}` : `exit code ${outcome.code}`;
671
- return fail("bundle", `\`${command.args.slice(0, 2).join(" ")}\` failed (${how})`, tailLines(transcript, LAST_LINES));
672
- }
673
- if (!exists(bundleOutput)) return fail("bundle", `the bundle command exited 0 but wrote no ${JS_BUNDLE_NAME} at ${bundleOutput}`);
674
- let hermes = false;
675
- let note;
676
- if (hermesEnabled ?? readHermesEnabled(root)) {
677
- const hermesc = hermescPath(root);
678
- if (!exists(hermesc)) note = `hermesc not found at ${hermesc}; embedding the plain JS bundle instead of Hermes bytecode`;
679
- else {
680
- const hbc = join(tmp, `${JS_BUNDLE_NAME}.hbc`);
681
- try {
682
- e.runFile(hermesc, hermescArgs({
683
- bundle: bundleOutput,
684
- out: hbc
685
- }));
686
- e.runFile("mv", [hbc, bundleOutput]);
687
- hermes = true;
688
- } catch (err) {
689
- return fail("hermesc", `hermesc failed on ${bundleOutput}: ${describe(err)}`);
690
- }
691
- }
692
- }
693
- try {
694
- e.runFile("cp", [bundleOutput, join(appCopy, JS_BUNDLE_NAME)]);
695
- e.runFile("cp", [
696
- "-R",
697
- `${assetsDest}/.`,
698
- `${appCopy}/`
699
- ]);
700
- } catch (err) {
701
- return fail("replace", `could not replace the JS bundle inside ${appCopy}: ${describe(err)}`);
702
- }
703
- try {
704
- e.runFile("codesign", [
705
- "--force",
706
- "--sign",
707
- "-",
708
- appCopy
709
- ]);
710
- } catch (err) {
711
- return fail("codesign", `codesign --force --sign - ${appCopy} failed: ${describe(err)}`);
712
- }
713
- logWriter?.write?.({
714
- src: "build",
715
- level: "info",
716
- msg: `JS swap done: ${hermes ? "hermes bytecode" : "plain JS"} into ${appCopy} in ${elapsed()}ms`,
717
- event: "js_swap"
718
- });
719
- const result = {
720
- ok: true,
721
- appPath: appCopy,
722
- tmpDir: tmp,
723
- hermes,
724
- durationMs: elapsed()
725
- };
726
- if (note) result.note = note;
727
- return result;
728
- }
729
- function describe(err) {
730
- return String(err?.message || err);
731
- }
732
- //#endregion
733
- //#region src/commands/dev-client.ts
734
- function devClientScheme(root, appPath = null, { exec = null } = {}) {
735
- if (!hasDevClient(root)) return void 0;
736
- const fromBundle = pickDevClientScheme(readBundleSchemes(appPath, { exec }));
737
- if (fromBundle) return fromBundle;
738
- const app = readJson(join(root, "app.json"));
739
- const raw = app?.expo?.scheme ?? app?.scheme ?? null;
740
- const scheme = Array.isArray(raw) ? raw.find((s) => typeof s === "string" && s.trim() !== "") : raw;
741
- if (typeof scheme !== "string" || scheme.trim() === "") return void 0;
742
- return scheme.trim();
743
- }
744
- function schemesFromInfoPlist(plist) {
745
- const types = plist?.CFBundleURLTypes;
746
- if (!Array.isArray(types)) return [];
747
- const out = [];
748
- for (const type of types) {
749
- const schemes = type?.CFBundleURLSchemes;
750
- if (Array.isArray(schemes)) out.push(...schemes.filter((s) => typeof s === "string" && s.trim() !== ""));
751
- }
752
- return out;
753
- }
754
- function readBundleSchemes(appPath, { exec = null } = {}) {
755
- if (typeof appPath !== "string" || appPath.trim() === "") return [];
756
- const e = exec || getExecutor();
757
- let out;
758
- try {
759
- out = e.runFile("plutil", [
760
- "-convert",
761
- "json",
762
- "-o",
763
- "-",
764
- join(appPath, "Info.plist")
765
- ]);
766
- } catch {
767
- return [];
768
- }
769
- try {
770
- return schemesFromInfoPlist(JSON.parse(String(out)));
771
- } catch {
772
- return [];
773
- }
774
- }
775
- const THIRD_PARTY_SCHEME = /^(?:fb\d+|com\.googleusercontent\.apps\.|msauth\.|msauthv2|twitterkit-|db-[a-z0-9]+$|spotify|snapchat|com\.facebook)/i;
776
- function pickDevClientScheme(schemes) {
777
- const all = (Array.isArray(schemes) ? schemes : []).filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim()).filter((s) => !/^(?:https?|mailto|tel|sms|itms(?:-apps)?)$/i.test(s));
778
- const expo = all.filter((s) => s.startsWith("exp+"));
779
- return (expo.length ? expo : all.filter((s) => !THIRD_PARTY_SCHEME.test(s))).toSorted((a, b) => b.length - a.length)[0] ?? null;
780
- }
781
- function hasDevClient(root) {
782
- const pkg = readJson(join(root, "package.json"));
783
- if ("expo-dev-client" in {
784
- ...pkg?.dependencies,
785
- ...pkg?.devDependencies
786
- }) return true;
787
- return isPackageResolvable(root, "expo-dev-client");
788
- }
789
- function readJson(file) {
790
- try {
791
- return JSON.parse(readFileSync(file, "utf-8"));
792
- } catch {
793
- return null;
794
- }
795
- }
796
- //#endregion
797
- export { easDeviceBuildRemedy as _, swapJsBundle as a, ensureWorkspaceStorageSafely as c, launchOutcomeRecord as d, noMetroMessage as f, warmMetro as g, sleep as h, detectEntryFile as i, gateShouldRetry as l, resolveMetroWithRetry as m, pickDevClientScheme as n, needsPrebuild as o, noMetroRemedy as p, schemesFromInfoPlist as r, runPrebuild as s, devClientScheme as t, isPhysicalDeviceRequest as u, isEasBuildFailure as v, resolveEasDevelopmentBuild as y };