stim 0.0.0 → 1.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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/android-cas-compiler.d.mts +1 -0
- package/dist/android-cas-compiler.mjs +71 -0
- package/dist/android-jc8MnUyL.mjs +514 -0
- package/dist/cache-manifest-4tH64LQ9.mjs +66 -0
- package/dist/cache-manifest.d.mts +27 -0
- package/dist/cache-manifest.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +27182 -0
- package/dist/collector-run.d.mts +119 -0
- package/dist/collector-run.mjs +539 -0
- package/dist/config-7kmtuhO2.mjs +350 -0
- package/dist/errors-B6W7FkxQ.mjs +15 -0
- package/dist/exec-bsN9MJXb.mjs +63 -0
- package/dist/metro-store-COLl1pOk.mjs +1249 -0
- package/dist/ndjson-DcAtEx_K.mjs +131 -0
- package/dist/ndjson-DvHSsNU9.d.mts +16 -0
- package/dist/process-output-C7IlIzd7.mjs +40 -0
- package/dist/project-Dit1CckW.mjs +234 -0
- package/dist/server-bare-BmTVVmBB.mjs +351 -0
- package/dist/server-expo-BZJm--wA.mjs +2 -0
- package/dist/server-expo-CHJe1r6j.mjs +311 -0
- package/dist/state-2nush3MK.mjs +269 -0
- package/dist/supervisor-run.d.mts +85 -0
- package/dist/supervisor-run.mjs +210 -0
- package/package.json +61 -8
- package/shim/android-cas-pch.cmake +35 -0
- package/shim/android-cas.gradle +29 -0
- package/shim/android-cas.toolchain.cmake +12 -0
- package/shim/android-optimizations.gradle +30 -0
- package/shim/bundle-response.cjs +46 -0
- package/shim/bundle-response.d.cts +5 -0
- package/shim/expo-metro-config.cjs +188 -0
- package/shim/metro-cache-generation.cjs +20 -0
- package/shim/metro-cache-generation.d.cts +5 -0
- package/skill/SKILL.md +14 -0
|
@@ -0,0 +1,1249 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
|
|
2
|
+
import { d as getProjectSettings, f as getRepoSettings, l as getConfigDir, m as loadConfig } from "./config-7kmtuhO2.mjs";
|
|
3
|
+
import { accessSync, chmodSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
4
|
+
import { chmodSync as chmodSync$1, existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, statSync as statSync$1, utimesSync } from "fs";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
6
|
+
import { METRO_NAMED_CACHE_LAYOUT, metroCacheRoot, registerCache, sharedStoreRoot, tagSharedStore } from "@stim-cli/core";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "node:path";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
//#region src/temporary.ts
|
|
12
|
+
function existingPath(path) {
|
|
13
|
+
const missing = [];
|
|
14
|
+
let current = resolve$1(path);
|
|
15
|
+
while (!lstatSync(current, { throwIfNoEntry: false })) {
|
|
16
|
+
missing.unshift(basename$1(current));
|
|
17
|
+
current = dirname$1(current);
|
|
18
|
+
}
|
|
19
|
+
const existing = realpathSync(current);
|
|
20
|
+
return {
|
|
21
|
+
existing,
|
|
22
|
+
resolved: join$1(existing, ...missing)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function filesystemDevice(path) {
|
|
26
|
+
return statSync(existingPath(path).existing).dev;
|
|
27
|
+
}
|
|
28
|
+
function outsideWorkingTree(path) {
|
|
29
|
+
let current = path;
|
|
30
|
+
while (true) {
|
|
31
|
+
if (lstatSync(join$1(current, ".git"), { throwIfNoEntry: false })) return false;
|
|
32
|
+
const parent = dirname$1(current);
|
|
33
|
+
if (parent === current) return true;
|
|
34
|
+
current = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function temporaryRoot(near) {
|
|
38
|
+
const reference = existingPath(near);
|
|
39
|
+
const insideReference = (path) => {
|
|
40
|
+
const rel = relative$1(reference.resolved, path);
|
|
41
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep$1}`) && !isAbsolute$1(rel);
|
|
42
|
+
};
|
|
43
|
+
const override = process.env.STIM_TMPDIR ?? loadConfig()?.tempDir;
|
|
44
|
+
if (override !== void 0) {
|
|
45
|
+
if (typeof override !== "string" || !isAbsolute$1(override)) throw new Error("STIM_TMPDIR / tempDir must be an absolute directory path.");
|
|
46
|
+
const { existing, resolved } = existingPath(override);
|
|
47
|
+
if (!statSync(existing).isDirectory() || !outsideWorkingTree(existing)) throw new Error(`Temporary directory ${override} must be outside Git working trees.`);
|
|
48
|
+
if (insideReference(resolved)) throw new Error(`Temporary directory ${override} must not be inside ${near}.`);
|
|
49
|
+
accessSync(existing, constants.W_OK);
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
const { existing } = reference;
|
|
53
|
+
const device = statSync(existing).dev;
|
|
54
|
+
const candidates = [tmpdir()];
|
|
55
|
+
let current = existing === reference.resolved || !statSync(existing).isDirectory() ? dirname$1(existing) : existing;
|
|
56
|
+
while (statSync(current).dev === device) {
|
|
57
|
+
candidates.push(current);
|
|
58
|
+
const parent = dirname$1(current);
|
|
59
|
+
if (parent === current) break;
|
|
60
|
+
current = parent;
|
|
61
|
+
}
|
|
62
|
+
for (const candidate of candidates) try {
|
|
63
|
+
const path = realpathSync(candidate);
|
|
64
|
+
if (statSync(path).dev !== device || insideReference(path) || !outsideWorkingTree(path)) continue;
|
|
65
|
+
accessSync(path, constants.W_OK);
|
|
66
|
+
return path;
|
|
67
|
+
} catch {}
|
|
68
|
+
throw new Error(`No writable temporary directory on the filesystem containing ${near}. Set STIM_TMPDIR or tempDir in ${join$1(getConfigDir(), "config.json")} to a directory outside Git working trees.`);
|
|
69
|
+
}
|
|
70
|
+
function makeTemporaryDirectory(near, prefix) {
|
|
71
|
+
const root = temporaryRoot(near);
|
|
72
|
+
mkdirSync(root, {
|
|
73
|
+
recursive: true,
|
|
74
|
+
mode: 448
|
|
75
|
+
});
|
|
76
|
+
return mkdtempSync(join$1(root, prefix));
|
|
77
|
+
}
|
|
78
|
+
function removeTemporaryEntry(path) {
|
|
79
|
+
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
80
|
+
if (stat?.isDirectory()) {
|
|
81
|
+
chmodSync(path, stat.mode | 448);
|
|
82
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) if (entry.isDirectory()) removeTemporaryEntry(join$1(path, entry.name));
|
|
83
|
+
}
|
|
84
|
+
rmSync(path, {
|
|
85
|
+
recursive: true,
|
|
86
|
+
force: true
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/worktree.ts
|
|
91
|
+
const CARRY_SKIP_BASENAMES = /* @__PURE__ */ new Set([".DerivedData"]);
|
|
92
|
+
function isCarrySkipped(rel) {
|
|
93
|
+
return /(^|\/)android\/build\/generated\/autolinking(\/|$)/.test(rel) || String(rel).split("/").some((seg) => CARRY_SKIP_BASENAMES.has(seg));
|
|
94
|
+
}
|
|
95
|
+
function gitCommonDir(cwd) {
|
|
96
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
97
|
+
"-C",
|
|
98
|
+
cwd,
|
|
99
|
+
"rev-parse",
|
|
100
|
+
"--path-format=absolute",
|
|
101
|
+
"--git-common-dir"
|
|
102
|
+
]);
|
|
103
|
+
return out ? out.trim() : null;
|
|
104
|
+
}
|
|
105
|
+
function isMainWorkingTree(path) {
|
|
106
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
107
|
+
"-C",
|
|
108
|
+
path,
|
|
109
|
+
"rev-parse",
|
|
110
|
+
"--path-format=absolute",
|
|
111
|
+
"--git-dir",
|
|
112
|
+
"--git-common-dir"
|
|
113
|
+
]);
|
|
114
|
+
if (!out) return false;
|
|
115
|
+
const [gitDir, commonDir] = out.trim().split("\n").map((line) => line.trim());
|
|
116
|
+
return Boolean(gitDir) && gitDir === commonDir;
|
|
117
|
+
}
|
|
118
|
+
function repoRoot(cwd) {
|
|
119
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
120
|
+
"-C",
|
|
121
|
+
cwd,
|
|
122
|
+
"rev-parse",
|
|
123
|
+
"--show-toplevel"
|
|
124
|
+
]);
|
|
125
|
+
return out ? out.trim() : null;
|
|
126
|
+
}
|
|
127
|
+
function warmWorktreePaths(cwd) {
|
|
128
|
+
const currentRoot = repoRoot(cwd);
|
|
129
|
+
if (!currentRoot) throw new Error("Not a git repository.");
|
|
130
|
+
const target = realpathSync$1(currentRoot);
|
|
131
|
+
if (isMainWorkingTree(target)) throw new Error("Run stim worktree warm from a linked worktree, not the main checkout.");
|
|
132
|
+
const entries = listWorktrees(target);
|
|
133
|
+
const current = entries.find((entry) => canonicalPath(entry.path) === target);
|
|
134
|
+
const main = entries.find((entry) => isMainWorkingTree(entry.path));
|
|
135
|
+
if (!current || !main) throw new Error("Could not identify the linked worktree and its main checkout.");
|
|
136
|
+
const root = realpathSync$1(main.path);
|
|
137
|
+
const sourceRoot = repoRoot(root);
|
|
138
|
+
const sourceCommon = gitCommonDir(root);
|
|
139
|
+
const targetCommon = gitCommonDir(target);
|
|
140
|
+
if (root === target || !sourceRoot || realpathSync$1(sourceRoot) !== root || !sourceCommon || !targetCommon || realpathSync$1(sourceCommon) !== realpathSync$1(targetCommon)) throw new Error("Could not verify that the main checkout belongs to this linked worktree.");
|
|
141
|
+
return {
|
|
142
|
+
root,
|
|
143
|
+
target,
|
|
144
|
+
common: realpathSync$1(targetCommon)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function matchesInclude(path, patterns) {
|
|
148
|
+
for (const pattern of patterns || []) {
|
|
149
|
+
const rooted = pattern.startsWith("/");
|
|
150
|
+
const escaped = (rooted ? pattern.slice(1) : pattern).replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*\*\//g, "::GLOBSTAR::").replace(/\*/g, "[^/]+").replace(/::GLOBSTAR::/g, "(?:.*/)?").replace(/\\\?/g, "[^/]");
|
|
151
|
+
if (new RegExp(`${rooted ? "^" : "(^|/)"}${escaped}$`).test(path)) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
function readWorktreeExclude(root) {
|
|
156
|
+
return readPatternFile(join(root, ".worktreeexclude"));
|
|
157
|
+
}
|
|
158
|
+
function readPatternFile(p) {
|
|
159
|
+
if (!existsSync$1(p)) return null;
|
|
160
|
+
return readFileSync$1(p, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
161
|
+
}
|
|
162
|
+
function canonicalPath(path) {
|
|
163
|
+
try {
|
|
164
|
+
return realpathSync$1(path);
|
|
165
|
+
} catch {
|
|
166
|
+
return resolve(path);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function nestedWorktreePaths(root) {
|
|
170
|
+
const source = canonicalPath(root);
|
|
171
|
+
const paths = /* @__PURE__ */ new Set();
|
|
172
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
173
|
+
"-C",
|
|
174
|
+
root,
|
|
175
|
+
"worktree",
|
|
176
|
+
"list",
|
|
177
|
+
"--porcelain"
|
|
178
|
+
]);
|
|
179
|
+
if (out === null) throw new Error("Could not list Git worktrees. Refusing to carry ignored files.");
|
|
180
|
+
for (const entry of parseWorktrees(out)) {
|
|
181
|
+
const rel = relative(source, canonicalPath(entry.path));
|
|
182
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
|
|
183
|
+
paths.add(rel.split(sep).join("/"));
|
|
184
|
+
}
|
|
185
|
+
return [...paths];
|
|
186
|
+
}
|
|
187
|
+
function overlapsNestedWorktree(rel, nestedPaths) {
|
|
188
|
+
const path = String(rel).replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
189
|
+
return nestedPaths.some((nested) => path === nested || path.startsWith(`${nested}/`) || nested.startsWith(`${path}/`));
|
|
190
|
+
}
|
|
191
|
+
function listTrackedPaths(dir) {
|
|
192
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
193
|
+
"-C",
|
|
194
|
+
dir,
|
|
195
|
+
"ls-files",
|
|
196
|
+
"-z"
|
|
197
|
+
]);
|
|
198
|
+
if (out === null) return null;
|
|
199
|
+
return out.split("\0").filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
function trackedGuard(dir) {
|
|
202
|
+
const paths = listTrackedPaths(dir);
|
|
203
|
+
if (paths === null) return {
|
|
204
|
+
known: false,
|
|
205
|
+
covers: () => false
|
|
206
|
+
};
|
|
207
|
+
const entries = new Set(paths);
|
|
208
|
+
const covered = new Set(paths);
|
|
209
|
+
for (const p of paths) for (let i = p.indexOf("/"); i !== -1; i = p.indexOf("/", i + 1)) covered.add(p.slice(0, i));
|
|
210
|
+
return {
|
|
211
|
+
known: true,
|
|
212
|
+
covers: (rel) => {
|
|
213
|
+
if (covered.has(rel)) return true;
|
|
214
|
+
for (let i = rel.indexOf("/"); i !== -1; i = rel.indexOf("/", i + 1)) if (entries.has(rel.slice(0, i))) return true;
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function listGitignoredEntries(root) {
|
|
220
|
+
const args = [
|
|
221
|
+
"-C",
|
|
222
|
+
root,
|
|
223
|
+
"ls-files",
|
|
224
|
+
"--others",
|
|
225
|
+
"--ignored",
|
|
226
|
+
"--exclude-standard",
|
|
227
|
+
"--directory",
|
|
228
|
+
"--no-empty-directory",
|
|
229
|
+
"-z"
|
|
230
|
+
];
|
|
231
|
+
const out = getExecutor().runFile("git", args);
|
|
232
|
+
if (!out) return [];
|
|
233
|
+
return out.split("\0").filter(Boolean).map((e) => e.endsWith("/") ? e.slice(0, -1) : e);
|
|
234
|
+
}
|
|
235
|
+
function listCarryableIgnoredEntries(root, patterns) {
|
|
236
|
+
const nested = nestedWorktreePaths(root);
|
|
237
|
+
return listGitignoredEntries(root).filter((rel) => !isCarrySkipped(rel) && !overlapsNestedWorktree(rel, nested) && !matchesInclude(rel, patterns));
|
|
238
|
+
}
|
|
239
|
+
function missingDestinationReason(target, rel) {
|
|
240
|
+
const parts = rel.split("/");
|
|
241
|
+
for (let i = 1; i < parts.length; i++) {
|
|
242
|
+
const parent = parts.slice(0, i).join("/");
|
|
243
|
+
const stat = lstatSync$1(join(target, parent), { throwIfNoEntry: false });
|
|
244
|
+
if (stat?.isSymbolicLink()) return `symlink ancestor: ${parent}`;
|
|
245
|
+
if (stat && !stat.isDirectory()) return `non-directory ancestor: ${parent}`;
|
|
246
|
+
}
|
|
247
|
+
return lstatSync$1(join(target, rel), { throwIfNoEntry: false }) ? "exists" : null;
|
|
248
|
+
}
|
|
249
|
+
function removeCopiedExclusions(path, rel) {
|
|
250
|
+
const entries = readdirSync$1(path, { withFileTypes: true });
|
|
251
|
+
const excluded = entries.filter((entry) => isCarrySkipped(`${rel}/${entry.name}`));
|
|
252
|
+
if (excluded.length > 0) {
|
|
253
|
+
const stat = lstatSync$1(path);
|
|
254
|
+
chmodSync$1(path, stat.mode | 448);
|
|
255
|
+
try {
|
|
256
|
+
for (const entry of excluded) removeTemporaryEntry(join(path, entry.name));
|
|
257
|
+
} finally {
|
|
258
|
+
utimesSync(path, stat.atime, stat.mtime);
|
|
259
|
+
chmodSync$1(path, stat.mode);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
const child = `${rel}/${entry.name}`;
|
|
264
|
+
if (entry.isDirectory() && !isCarrySkipped(child)) removeCopiedExclusions(join(path, entry.name), child);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function cloneIgnoredEntries({ root, target, patterns }) {
|
|
268
|
+
const copied = [];
|
|
269
|
+
const skipped = [];
|
|
270
|
+
const failed = [];
|
|
271
|
+
let cloned = true;
|
|
272
|
+
const guard = trackedGuard(target);
|
|
273
|
+
if (!guard.known) throw new Error("Could not list the destination's tracked files.");
|
|
274
|
+
const nested = nestedWorktreePaths(target);
|
|
275
|
+
for (const rel of listCarryableIgnoredEntries(root, patterns)) {
|
|
276
|
+
const from = join(root, rel);
|
|
277
|
+
const to = join(target, rel);
|
|
278
|
+
try {
|
|
279
|
+
const reason = guard.covers(rel) ? "tracked" : overlapsNestedWorktree(rel, nested) ? "nested worktree" : missingDestinationReason(target, rel);
|
|
280
|
+
if (reason) {
|
|
281
|
+
skipped.push({
|
|
282
|
+
file: rel,
|
|
283
|
+
reason
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
mkdirSync$1(dirname(to), { recursive: true });
|
|
288
|
+
try {
|
|
289
|
+
getExecutor().runFile("cp", [
|
|
290
|
+
"-Rc",
|
|
291
|
+
from,
|
|
292
|
+
to
|
|
293
|
+
]);
|
|
294
|
+
} catch {
|
|
295
|
+
removeTemporaryEntry(to);
|
|
296
|
+
getExecutor().runFile("cp", [
|
|
297
|
+
"-R",
|
|
298
|
+
from,
|
|
299
|
+
to
|
|
300
|
+
]);
|
|
301
|
+
cloned = false;
|
|
302
|
+
}
|
|
303
|
+
if (lstatSync$1(to).isDirectory()) removeCopiedExclusions(to, rel);
|
|
304
|
+
copied.push(rel);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
failed.push({
|
|
307
|
+
file: rel,
|
|
308
|
+
error: String(e?.message || e)
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
copied,
|
|
314
|
+
skipped,
|
|
315
|
+
failed,
|
|
316
|
+
cloned
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function podsOutOfSync(target, copiedEntries, { read = readFileSync$1 } = {}) {
|
|
320
|
+
const problems = [];
|
|
321
|
+
for (const rel of copiedEntries || []) {
|
|
322
|
+
if (rel !== "Pods" && !rel.endsWith("/Pods")) continue;
|
|
323
|
+
const iosDir = rel === "Pods" ? "" : rel.slice(0, -5);
|
|
324
|
+
const manifest = join(target, rel, "Manifest.lock");
|
|
325
|
+
const podfileLock = join(target, iosDir, "Podfile.lock");
|
|
326
|
+
if (!existsSync$1(manifest)) continue;
|
|
327
|
+
if (!existsSync$1(podfileLock)) {
|
|
328
|
+
problems.push({
|
|
329
|
+
dir: iosDir || ".",
|
|
330
|
+
reason: "missing"
|
|
331
|
+
});
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
if (read(manifest, "utf-8") !== read(podfileLock, "utf-8")) problems.push({
|
|
336
|
+
dir: iosDir || ".",
|
|
337
|
+
reason: "mismatch"
|
|
338
|
+
});
|
|
339
|
+
} catch {}
|
|
340
|
+
}
|
|
341
|
+
return problems;
|
|
342
|
+
}
|
|
343
|
+
const LOCKFILE_NAMES = [
|
|
344
|
+
"package-lock.json",
|
|
345
|
+
"yarn.lock",
|
|
346
|
+
"pnpm-lock.yaml",
|
|
347
|
+
"bun.lock",
|
|
348
|
+
"bun.lockb"
|
|
349
|
+
];
|
|
350
|
+
function depsOutOfSync(root, target, copiedEntries, { read = readFileSync$1 } = {}) {
|
|
351
|
+
const problems = [];
|
|
352
|
+
for (const rel of copiedEntries || []) {
|
|
353
|
+
if (rel !== "node_modules" && !rel.endsWith("/node_modules")) continue;
|
|
354
|
+
const dir = rel === "node_modules" ? "" : rel.slice(0, -13);
|
|
355
|
+
for (const name of LOCKFILE_NAMES) {
|
|
356
|
+
const source = join(root, dir, name);
|
|
357
|
+
const branch = join(target, dir, name);
|
|
358
|
+
if (!existsSync$1(source) || !existsSync$1(branch)) continue;
|
|
359
|
+
try {
|
|
360
|
+
if (read(source, "utf-8") !== read(branch, "utf-8")) problems.push({
|
|
361
|
+
dir: dir || ".",
|
|
362
|
+
lockfile: name
|
|
363
|
+
});
|
|
364
|
+
} catch {}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return problems;
|
|
369
|
+
}
|
|
370
|
+
const FINGERPRINT_INPUT_FILES = [
|
|
371
|
+
"app.json",
|
|
372
|
+
"app.config.ts",
|
|
373
|
+
"app.config.js",
|
|
374
|
+
"app.config.mjs",
|
|
375
|
+
"package.json"
|
|
376
|
+
];
|
|
377
|
+
function dirtyFingerprintFiles(root) {
|
|
378
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
379
|
+
"-C",
|
|
380
|
+
root,
|
|
381
|
+
"status",
|
|
382
|
+
"--porcelain",
|
|
383
|
+
"--",
|
|
384
|
+
...FINGERPRINT_INPUT_FILES
|
|
385
|
+
]);
|
|
386
|
+
if (out === null || out.trim() === "") return [];
|
|
387
|
+
return out.split("\n").map((line) => normalizePorcelainLine(line.trimEnd())).filter((line) => line !== "").map((line) => line.slice(3).trim()).filter((path) => path !== "");
|
|
388
|
+
}
|
|
389
|
+
function hasUncommittedWork(dir) {
|
|
390
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
391
|
+
"-C",
|
|
392
|
+
dir,
|
|
393
|
+
"status",
|
|
394
|
+
"--porcelain"
|
|
395
|
+
]);
|
|
396
|
+
if (out === null) return null;
|
|
397
|
+
return out.trim().length > 0;
|
|
398
|
+
}
|
|
399
|
+
function dirtyPaths(dir, { limit = 10 } = {}) {
|
|
400
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
401
|
+
"-C",
|
|
402
|
+
dir,
|
|
403
|
+
"status",
|
|
404
|
+
"--porcelain"
|
|
405
|
+
]);
|
|
406
|
+
if (out === null) return [];
|
|
407
|
+
return out.split("\n").map((l) => normalizePorcelainLine(l.trimEnd())).filter(Boolean).slice(0, limit);
|
|
408
|
+
}
|
|
409
|
+
function normalizePorcelainLine(line) {
|
|
410
|
+
if (line === "" || line[2] === " ") return line;
|
|
411
|
+
return ` ${line}`;
|
|
412
|
+
}
|
|
413
|
+
function restoreFile(dir, file) {
|
|
414
|
+
return getExecutor().runFileQuiet("git", [
|
|
415
|
+
"-C",
|
|
416
|
+
dir,
|
|
417
|
+
"checkout",
|
|
418
|
+
"--",
|
|
419
|
+
file
|
|
420
|
+
]) !== null;
|
|
421
|
+
}
|
|
422
|
+
function isPodInstallChurn(paths) {
|
|
423
|
+
if (!paths || paths.length === 0) return false;
|
|
424
|
+
return paths.every((line) => /(?:^|\/)(?:Podfile\.lock|project\.pbxproj)$/.test(line.slice(3).trim()));
|
|
425
|
+
}
|
|
426
|
+
const SAFE_BRANCH_NAME = /^[A-Za-z0-9._/-]+$/;
|
|
427
|
+
function unpushedCommits(dir) {
|
|
428
|
+
const exec = getExecutor();
|
|
429
|
+
const branch = exec.runFileQuiet("git", [
|
|
430
|
+
"-C",
|
|
431
|
+
dir,
|
|
432
|
+
"symbolic-ref",
|
|
433
|
+
"--quiet",
|
|
434
|
+
"--short",
|
|
435
|
+
"HEAD"
|
|
436
|
+
]);
|
|
437
|
+
const own = branch === null ? "" : branch.trim();
|
|
438
|
+
const protection = own && SAFE_BRANCH_NAME.test(own) ? [
|
|
439
|
+
"--remotes",
|
|
440
|
+
`--exclude=${own}`,
|
|
441
|
+
"--branches"
|
|
442
|
+
] : ["--remotes"];
|
|
443
|
+
if (!own && exec.runFileQuiet("git", [
|
|
444
|
+
"-C",
|
|
445
|
+
dir,
|
|
446
|
+
"rev-parse",
|
|
447
|
+
"--abbrev-ref",
|
|
448
|
+
"HEAD"
|
|
449
|
+
]) === "HEAD") protection.push("--branches");
|
|
450
|
+
const out = exec.runFileQuiet("git", [
|
|
451
|
+
"-C",
|
|
452
|
+
dir,
|
|
453
|
+
"log",
|
|
454
|
+
"--oneline",
|
|
455
|
+
"HEAD",
|
|
456
|
+
"--not",
|
|
457
|
+
...protection
|
|
458
|
+
]);
|
|
459
|
+
if (out === null) return null;
|
|
460
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
461
|
+
}
|
|
462
|
+
function hasRemote(dir) {
|
|
463
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
464
|
+
"-C",
|
|
465
|
+
dir,
|
|
466
|
+
"remote"
|
|
467
|
+
]);
|
|
468
|
+
return Boolean(out && out.trim().length > 0);
|
|
469
|
+
}
|
|
470
|
+
function branchExists(cwd, branch) {
|
|
471
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
472
|
+
"-C",
|
|
473
|
+
cwd,
|
|
474
|
+
"rev-parse",
|
|
475
|
+
"--verify",
|
|
476
|
+
"--quiet",
|
|
477
|
+
`refs/heads/${branch}`
|
|
478
|
+
]);
|
|
479
|
+
return Boolean(out);
|
|
480
|
+
}
|
|
481
|
+
function resolveFullRef(cwd, ref) {
|
|
482
|
+
try {
|
|
483
|
+
const out = getExecutor().runFile("git", [
|
|
484
|
+
"-C",
|
|
485
|
+
cwd,
|
|
486
|
+
"rev-parse",
|
|
487
|
+
"--verify",
|
|
488
|
+
"--quiet",
|
|
489
|
+
"--end-of-options",
|
|
490
|
+
`${ref}^{commit}`
|
|
491
|
+
]);
|
|
492
|
+
return out && out.trim() ? out.trim() : null;
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function removeWorktree(path, { force = false } = {}) {
|
|
498
|
+
const args = [
|
|
499
|
+
"-C",
|
|
500
|
+
path,
|
|
501
|
+
"worktree",
|
|
502
|
+
"remove",
|
|
503
|
+
...force ? ["--force"] : [],
|
|
504
|
+
"--",
|
|
505
|
+
path
|
|
506
|
+
];
|
|
507
|
+
getExecutor().runFile("git", args);
|
|
508
|
+
}
|
|
509
|
+
function deleteBranch(cwd, branch, expectedSha) {
|
|
510
|
+
if (!SAFE_BRANCH_NAME.test(branch) || branch.startsWith("-")) throw new Error(`Refusing branch ${JSON.stringify(branch)}: it is not a safe local branch name.`);
|
|
511
|
+
getExecutor().runFile("git", [
|
|
512
|
+
"-C",
|
|
513
|
+
cwd,
|
|
514
|
+
"update-ref",
|
|
515
|
+
"-d",
|
|
516
|
+
`refs/heads/${branch}`,
|
|
517
|
+
expectedSha
|
|
518
|
+
]);
|
|
519
|
+
}
|
|
520
|
+
function parseWorktrees(out) {
|
|
521
|
+
const entries = [];
|
|
522
|
+
let current = {};
|
|
523
|
+
for (const line of out.split("\n")) if (line.startsWith("worktree ")) {
|
|
524
|
+
if (current.path) entries.push(current);
|
|
525
|
+
current = { path: line.slice(9) };
|
|
526
|
+
} else if (line.startsWith("branch ")) current.branch = line.slice(7).replace("refs/heads/", "");
|
|
527
|
+
if (current.path) entries.push(current);
|
|
528
|
+
return entries;
|
|
529
|
+
}
|
|
530
|
+
function listWorktrees(cwd) {
|
|
531
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
532
|
+
"-C",
|
|
533
|
+
cwd,
|
|
534
|
+
"worktree",
|
|
535
|
+
"list",
|
|
536
|
+
"--porcelain"
|
|
537
|
+
]);
|
|
538
|
+
return out ? parseWorktrees(out) : [];
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/optimizations.ts
|
|
542
|
+
const OPTIMIZATION_SHAPES = {
|
|
543
|
+
optimizations: "object",
|
|
544
|
+
"optimizations.buildCache": "boolean",
|
|
545
|
+
"optimizations.remoteBuildCache": "boolean",
|
|
546
|
+
"optimizations.releaseBundleSwap": "boolean",
|
|
547
|
+
"optimizations.metroSharedCache": "boolean",
|
|
548
|
+
"optimizations.ios": "object",
|
|
549
|
+
"optimizations.ios.compilationCache": "boolean",
|
|
550
|
+
"optimizations.ios.swiftCompilationCache": "boolean",
|
|
551
|
+
"optimizations.ios.prefixMapping": "boolean",
|
|
552
|
+
"optimizations.android": "object",
|
|
553
|
+
"optimizations.android.compilerCache": "string",
|
|
554
|
+
"optimizations.android.casToolchain": "path",
|
|
555
|
+
"optimizations.android.pch": "string",
|
|
556
|
+
"optimizations.android.gradleBuildCache": "boolean",
|
|
557
|
+
"optimizations.android.targetAbiOnly": "boolean"
|
|
558
|
+
};
|
|
559
|
+
function optimizationBuildProfile(platform, options) {
|
|
560
|
+
const selected = platform === "ios" ? options.ios : { pch: options.android.pch };
|
|
561
|
+
const defaults = platform === "ios" ? {
|
|
562
|
+
compilationCache: true,
|
|
563
|
+
swiftCompilationCache: false,
|
|
564
|
+
prefixMapping: true
|
|
565
|
+
} : { pch: "auto" };
|
|
566
|
+
if (JSON.stringify(selected) === JSON.stringify(defaults)) return void 0;
|
|
567
|
+
return `opt-${createHash("sha256").update(JSON.stringify(selected)).digest("hex").slice(0, 16)}`;
|
|
568
|
+
}
|
|
569
|
+
function resolveOptimizations(settings = {}, env = process.env) {
|
|
570
|
+
function choice(path, choices, fallback) {
|
|
571
|
+
const raw = optimizationValue(settings, path);
|
|
572
|
+
if (raw === void 0) return fallback;
|
|
573
|
+
if (typeof raw !== "string" || !choices.includes(raw)) throw new Error(`Invalid optimizations.${path}. Expected ${choices.join(", ")}.`);
|
|
574
|
+
return raw;
|
|
575
|
+
}
|
|
576
|
+
const compiler = choice("android.compilerCache", [
|
|
577
|
+
"auto",
|
|
578
|
+
"ccache",
|
|
579
|
+
"cas",
|
|
580
|
+
"none"
|
|
581
|
+
], "auto");
|
|
582
|
+
const manifest = env.STIM_ANDROID_CAS_TOOLCHAIN || optimizationValue(settings, "android.casToolchain");
|
|
583
|
+
if (manifest !== void 0 && (typeof manifest !== "string" || !isAbsolute$1(manifest) || /[\r\n\0]/.test(manifest))) throw new Error("Invalid Android CAS toolchain. Expected an absolute path to the toolchain JSON manifest.");
|
|
584
|
+
const compilerCache = compiler === "auto" ? manifest ? "cas" : "ccache" : compiler;
|
|
585
|
+
if (compilerCache === "cas" && !manifest) throw new Error("optimizations.android.compilerCache=cas requires optimizations.android.casToolchain or STIM_ANDROID_CAS_TOOLCHAIN.");
|
|
586
|
+
return {
|
|
587
|
+
buildCache: optimizationBoolean(settings, "buildCache"),
|
|
588
|
+
remoteBuildCache: optimizationBoolean(settings, "remoteBuildCache"),
|
|
589
|
+
releaseBundleSwap: optimizationBoolean(settings, "releaseBundleSwap"),
|
|
590
|
+
metroSharedCache: optimizationBoolean(settings, "metroSharedCache"),
|
|
591
|
+
ios: {
|
|
592
|
+
compilationCache: optimizationBoolean(settings, "ios.compilationCache"),
|
|
593
|
+
swiftCompilationCache: optimizationBoolean(settings, "ios.swiftCompilationCache", false),
|
|
594
|
+
prefixMapping: optimizationBoolean(settings, "ios.prefixMapping")
|
|
595
|
+
},
|
|
596
|
+
android: {
|
|
597
|
+
compilerCache,
|
|
598
|
+
casToolchain: manifest ?? null,
|
|
599
|
+
pch: choice("android.pch", [
|
|
600
|
+
"auto",
|
|
601
|
+
"on",
|
|
602
|
+
"off"
|
|
603
|
+
], "auto"),
|
|
604
|
+
gradleBuildCache: optimizationBoolean(settings, "android.gradleBuildCache"),
|
|
605
|
+
targetAbiOnly: optimizationBoolean(settings, "android.targetAbiOnly")
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function artifactCachePolicy(options, reuse, release) {
|
|
610
|
+
return {
|
|
611
|
+
read: reuse && options.buildCache && (!release || options.releaseBundleSwap),
|
|
612
|
+
write: options.buildCache,
|
|
613
|
+
remote: options.buildCache && options.remoteBuildCache
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function optimizationValue(settings, path) {
|
|
617
|
+
let node = settings;
|
|
618
|
+
for (const key of ["optimizations", ...path.split(".")]) {
|
|
619
|
+
if (node === void 0) return void 0;
|
|
620
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) throw new Error("Invalid optimizations setting. Expected nested objects.");
|
|
621
|
+
node = node[key];
|
|
622
|
+
}
|
|
623
|
+
return node;
|
|
624
|
+
}
|
|
625
|
+
function optimizationBoolean(settings, path, fallback = true) {
|
|
626
|
+
const raw = optimizationValue(settings, path);
|
|
627
|
+
if (raw === void 0) return fallback;
|
|
628
|
+
if (typeof raw !== "boolean") throw new Error(`Invalid optimizations.${path}. Expected true or false.`);
|
|
629
|
+
return raw;
|
|
630
|
+
}
|
|
631
|
+
function resolveMetroSharedCache(settings) {
|
|
632
|
+
return optimizationBoolean(settings, "metroSharedCache");
|
|
633
|
+
}
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/engine/metro-reach.ts
|
|
636
|
+
const TUNNEL_MODES = [
|
|
637
|
+
"auto",
|
|
638
|
+
"off",
|
|
639
|
+
"expo",
|
|
640
|
+
"cloudflared",
|
|
641
|
+
"ngrok"
|
|
642
|
+
];
|
|
643
|
+
const PUBLIC_METRO_ENV = "STIM_METRO_PUBLIC_URL";
|
|
644
|
+
const MANAGED_PROVIDERS = ["ngrok", "cloudflared"];
|
|
645
|
+
const NAMED = {
|
|
646
|
+
cloudflared: "`cloudflared` (brew install cloudflared)",
|
|
647
|
+
ngrok: "`ngrok` (brew install ngrok, then `ngrok config add-authtoken <token>`)"
|
|
648
|
+
};
|
|
649
|
+
function planMetroReach({ mode, metroPort, publicUrl = null, isExpo, available = [] }) {
|
|
650
|
+
const named = publicUrl?.trim().replace(/\/+$/, "") || null;
|
|
651
|
+
if (mode === "off") return {
|
|
652
|
+
origin: `http://localhost:${metroPort}`,
|
|
653
|
+
gate: false
|
|
654
|
+
};
|
|
655
|
+
if (named) return {
|
|
656
|
+
origin: named,
|
|
657
|
+
gate: true
|
|
658
|
+
};
|
|
659
|
+
if (mode === "expo") {
|
|
660
|
+
if (!isExpo) return {
|
|
661
|
+
failed: "metro.tunnel is \"expo\", but this workspace does not run an Expo dev server.",
|
|
662
|
+
remedy: "Use \"auto\" to let stim start a tunnel, or \"off\" if the device shares this machine."
|
|
663
|
+
};
|
|
664
|
+
return { expoTunnel: true };
|
|
665
|
+
}
|
|
666
|
+
if (mode === "cloudflared" || mode === "ngrok") {
|
|
667
|
+
if (!available.includes(mode)) return {
|
|
668
|
+
failed: `metro.tunnel is "${mode}", but ${mode} is not on PATH.`,
|
|
669
|
+
remedy: `Install ${NAMED[mode] ?? mode}, or set metro.tunnel to "auto".`
|
|
670
|
+
};
|
|
671
|
+
return { start: mode };
|
|
672
|
+
}
|
|
673
|
+
const provider = available[0];
|
|
674
|
+
if (provider) return { start: provider };
|
|
675
|
+
return {
|
|
676
|
+
failed: `A remote device cannot reach this workspace's Metro on port ${metroPort}, and no tunnel is available to give it one.`,
|
|
677
|
+
remedy: `Install ${NAMED.ngrok} or ${NAMED.cloudflared} and Stim will manage the tunnel for you. If you already have one, set metro.publicUrl to its URL. If the device shares this machine (a local \`agent-device proxy\`), set metro.tunnel to "off".`
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function detectProviders(onPath) {
|
|
681
|
+
return MANAGED_PROVIDERS.filter((p) => onPath(p));
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/settings.ts
|
|
685
|
+
function isPlainObject(v) {
|
|
686
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
687
|
+
}
|
|
688
|
+
function mergeSettingsLayers(layers) {
|
|
689
|
+
const out = {};
|
|
690
|
+
for (const layer of layers) {
|
|
691
|
+
if (!isPlainObject(layer)) continue;
|
|
692
|
+
for (const [key, value] of Object.entries(layer)) if (isPlainObject(value) && isPlainObject(out[key])) out[key] = mergeSettingsLayers([out[key], value]);
|
|
693
|
+
else if (!(key in out)) out[key] = value;
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
const SETTING_SHAPE_RULES = {
|
|
698
|
+
boolean: {
|
|
699
|
+
expected: "true or false",
|
|
700
|
+
accepts: (value) => typeof value === "boolean"
|
|
701
|
+
},
|
|
702
|
+
string: {
|
|
703
|
+
expected: "a string",
|
|
704
|
+
accepts: (value) => typeof value === "string"
|
|
705
|
+
},
|
|
706
|
+
path: {
|
|
707
|
+
expected: "a string path",
|
|
708
|
+
accepts: (value) => typeof value === "string"
|
|
709
|
+
},
|
|
710
|
+
strings: {
|
|
711
|
+
expected: "an array of strings",
|
|
712
|
+
accepts: (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
713
|
+
},
|
|
714
|
+
number: {
|
|
715
|
+
expected: "a number",
|
|
716
|
+
accepts: (value) => typeof value === "number"
|
|
717
|
+
},
|
|
718
|
+
object: {
|
|
719
|
+
expected: "an object",
|
|
720
|
+
accepts: isPlainObject
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
const SETTING_SHAPES = {
|
|
724
|
+
...OPTIMIZATION_SHAPES,
|
|
725
|
+
"ios.deviceType": "string",
|
|
726
|
+
"ios.runtime": "string",
|
|
727
|
+
"ios.configuration": "string",
|
|
728
|
+
"ios.remote": "string",
|
|
729
|
+
"ios.simslimProfile": "path",
|
|
730
|
+
"ios.signingIdentity": "string",
|
|
731
|
+
"ios.signingIdentitySha1": "string",
|
|
732
|
+
"ios.lanHost": "string",
|
|
733
|
+
"android.systemImage": "string",
|
|
734
|
+
"android.dataPartitionSizeGb": "number",
|
|
735
|
+
"android.avdConfigFile": "path",
|
|
736
|
+
"android.avdConfig": "object",
|
|
737
|
+
"android.variant": "string",
|
|
738
|
+
"android.keystore": "path",
|
|
739
|
+
"android.keystorePassword": "string",
|
|
740
|
+
"android.remote": "string",
|
|
741
|
+
"metro.tunnel": "string",
|
|
742
|
+
"metro.ngrokUrl": "string",
|
|
743
|
+
"metro.publicUrl": "string",
|
|
744
|
+
"worktree.exclude": "strings",
|
|
745
|
+
"cache.provider": "string",
|
|
746
|
+
"cache.options": "object",
|
|
747
|
+
caches: "strings"
|
|
748
|
+
};
|
|
749
|
+
const KNOWN_SETTINGS = new Set(Object.keys(SETTING_SHAPES));
|
|
750
|
+
function settingValueAt(settings, path) {
|
|
751
|
+
let node = settings;
|
|
752
|
+
for (const segment of path.split(".")) {
|
|
753
|
+
if (!isPlainObject(node)) return void 0;
|
|
754
|
+
node = node[segment];
|
|
755
|
+
}
|
|
756
|
+
return node;
|
|
757
|
+
}
|
|
758
|
+
function settingShapeErrors(settings) {
|
|
759
|
+
const errors = [];
|
|
760
|
+
for (const [path, shape] of Object.entries(SETTING_SHAPES)) {
|
|
761
|
+
const value = settingValueAt(settings, path);
|
|
762
|
+
if (value === void 0) continue;
|
|
763
|
+
const rule = SETTING_SHAPE_RULES[shape];
|
|
764
|
+
if (rule.accepts(value)) continue;
|
|
765
|
+
errors.push(`Invalid ${path} setting ${JSON.stringify(value)}. Expected ${rule.expected}.`);
|
|
766
|
+
}
|
|
767
|
+
return errors;
|
|
768
|
+
}
|
|
769
|
+
const SETTING_SHAPE_REMEDY = "Run `stim guide settings` for the shape each setting takes.";
|
|
770
|
+
const MAX_ANDROID_DATA_PARTITION_SIZE_GB = 16384;
|
|
771
|
+
function validateAndroidDataPartitionSizeGb(raw) {
|
|
772
|
+
if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw < 6 || raw > 16384) throw new Error(`Invalid android.dataPartitionSizeGb setting ${JSON.stringify(raw)}. Expected an integer from 6 through ${MAX_ANDROID_DATA_PARTITION_SIZE_GB} GiB.`);
|
|
773
|
+
return raw;
|
|
774
|
+
}
|
|
775
|
+
function androidDataPartitionSizeBytes(sizeGb) {
|
|
776
|
+
return validateAndroidDataPartitionSizeGb(sizeGb) * 1024 ** 3;
|
|
777
|
+
}
|
|
778
|
+
function androidDataPartitionSizeGbRaw(settings) {
|
|
779
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.android)) return void 0;
|
|
780
|
+
return settings.android.dataPartitionSizeGb;
|
|
781
|
+
}
|
|
782
|
+
function androidDataPartitionSizeGbSetting(settings) {
|
|
783
|
+
const raw = androidDataPartitionSizeGbRaw(settings);
|
|
784
|
+
if (raw === void 0) return 8;
|
|
785
|
+
return validateAndroidDataPartitionSizeGb(raw);
|
|
786
|
+
}
|
|
787
|
+
function androidDataPartitionSizeGbSettingError(settings) {
|
|
788
|
+
try {
|
|
789
|
+
androidDataPartitionSizeGbSetting(settings);
|
|
790
|
+
return null;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
return String(error?.message || error);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function yesNoAvdValue(value) {
|
|
796
|
+
if (value === true || value === "true" || value === "yes") return "yes";
|
|
797
|
+
if (value === false || value === "false" || value === "no") return "no";
|
|
798
|
+
throw new Error("expected true, false, \"yes\", or \"no\"");
|
|
799
|
+
}
|
|
800
|
+
function boundedIntegerAvdRule(min, max, unit) {
|
|
801
|
+
return {
|
|
802
|
+
help: `integer ${min}-${max}${unit ? ` ${unit}` : ""}`,
|
|
803
|
+
parse: (value) => {
|
|
804
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" && /^(0|[1-9]\d*)$/.test(value) ? Number(value) : NaN;
|
|
805
|
+
if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) throw new Error(`expected an integer from ${min} through ${max}${unit ? ` ${unit}` : ""}`);
|
|
806
|
+
return String(parsed);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function enumAvdRule(values) {
|
|
811
|
+
return {
|
|
812
|
+
help: values.join("|"),
|
|
813
|
+
parse: (value) => {
|
|
814
|
+
if (typeof value !== "string" || !values.includes(value)) throw new Error(`expected one of: ${values.join(", ")}`);
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const ANDROID_AVD_CONFIG_RULES = {
|
|
820
|
+
"hw.cpu.ncore": boundedIntegerAvdRule(1, 64),
|
|
821
|
+
"hw.gpu.mode": enumAvdRule([
|
|
822
|
+
"auto",
|
|
823
|
+
"host",
|
|
824
|
+
"software",
|
|
825
|
+
"lavapipe",
|
|
826
|
+
"swiftshader",
|
|
827
|
+
"swangle"
|
|
828
|
+
]),
|
|
829
|
+
"hw.initialOrientation": enumAvdRule(["portrait", "landscape"]),
|
|
830
|
+
"hw.lcd.density": boundedIntegerAvdRule(72, 1e3, "dpi"),
|
|
831
|
+
"hw.lcd.vsync": boundedIntegerAvdRule(1, 1e3, "Hz"),
|
|
832
|
+
"hw.ramSize": boundedIntegerAvdRule(1536, 8192, "MB"),
|
|
833
|
+
"hw.screen": enumAvdRule([
|
|
834
|
+
"no-touch",
|
|
835
|
+
"touch",
|
|
836
|
+
"multi-touch"
|
|
837
|
+
]),
|
|
838
|
+
"runtime.network.latency": enumAvdRule([
|
|
839
|
+
"none",
|
|
840
|
+
"gsm",
|
|
841
|
+
"hscsd",
|
|
842
|
+
"gprs",
|
|
843
|
+
"edge",
|
|
844
|
+
"umts",
|
|
845
|
+
"hsdpa",
|
|
846
|
+
"lte",
|
|
847
|
+
"evdo",
|
|
848
|
+
"5g"
|
|
849
|
+
]),
|
|
850
|
+
"runtime.network.speed": enumAvdRule([
|
|
851
|
+
"gsm",
|
|
852
|
+
"hscsd",
|
|
853
|
+
"gprs",
|
|
854
|
+
"edge",
|
|
855
|
+
"umts",
|
|
856
|
+
"hsdpa",
|
|
857
|
+
"lte",
|
|
858
|
+
"evdo",
|
|
859
|
+
"5g",
|
|
860
|
+
"full"
|
|
861
|
+
]),
|
|
862
|
+
"vm.heapSize": boundedIntegerAvdRule(16, 4096, "MB")
|
|
863
|
+
};
|
|
864
|
+
const YES_NO_AVD_RULE = {
|
|
865
|
+
help: "true|false|yes|no",
|
|
866
|
+
parse: yesNoAvdValue
|
|
867
|
+
};
|
|
868
|
+
for (const key of [
|
|
869
|
+
"hw.accelerometer",
|
|
870
|
+
"hw.accelerometer_uncalibrated",
|
|
871
|
+
"hw.audioInput",
|
|
872
|
+
"hw.audioOutput",
|
|
873
|
+
"hw.battery",
|
|
874
|
+
"hw.dPad",
|
|
875
|
+
"hw.gps",
|
|
876
|
+
"hw.gpu.enabled",
|
|
877
|
+
"hw.gyroscope",
|
|
878
|
+
"hw.keyboard",
|
|
879
|
+
"hw.mainKeys",
|
|
880
|
+
"hw.rotaryInput",
|
|
881
|
+
"hw.trackBall",
|
|
882
|
+
"showDeviceFrame"
|
|
883
|
+
]) ANDROID_AVD_CONFIG_RULES[key] = YES_NO_AVD_RULE;
|
|
884
|
+
const ANDROID_AVD_CONFIG_KEYS = Object.freeze(Object.keys(ANDROID_AVD_CONFIG_RULES).toSorted());
|
|
885
|
+
const ANDROID_AVD_CONFIG_HELP = Object.freeze(ANDROID_AVD_CONFIG_KEYS.map((key) => `${key}: ${ANDROID_AVD_CONFIG_RULES[key].help}`));
|
|
886
|
+
function isSafeAndroidAvdConfigKey(key) {
|
|
887
|
+
return Object.hasOwn(ANDROID_AVD_CONFIG_RULES, key);
|
|
888
|
+
}
|
|
889
|
+
function normalizedAndroidAvdConfigEntry(key, value) {
|
|
890
|
+
if (typeof key !== "string" || key !== key.trim() || !isSafeAndroidAvdConfigKey(key)) throw new Error(`Unsupported android.avdConfig key ${JSON.stringify(key)}. Use a documented safe hardware override; identity, path, storage, image, snapshot, and boot-lifecycle keys are protected.`);
|
|
891
|
+
if (typeof value === "string" && /[\r\n\0]/.test(value)) throw new Error(`Invalid android.avdConfig value for ${key}: expected one line.`);
|
|
892
|
+
try {
|
|
893
|
+
return [key, ANDROID_AVD_CONFIG_RULES[key].parse(value)];
|
|
894
|
+
} catch (error) {
|
|
895
|
+
throw new Error(`Invalid android.avdConfig value for ${key}: ${String(error?.message || error)}.`, { cause: error });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function parseAndroidAvdConfigIni(contents) {
|
|
899
|
+
const entries = {};
|
|
900
|
+
const lines = contents.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
901
|
+
for (let index = 0; index < lines.length; index++) {
|
|
902
|
+
const line = String(lines[index] ?? "").trim();
|
|
903
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
904
|
+
const separator = line.indexOf("=");
|
|
905
|
+
if (separator <= 0) throw new Error(`Invalid android.avdConfigFile line ${index + 1}: expected key=value.`);
|
|
906
|
+
const [key, value] = normalizedAndroidAvdConfigEntry(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
907
|
+
if (Object.hasOwn(entries, key)) throw new Error(`Invalid android.avdConfigFile line ${index + 1}: duplicate key ${key}.`);
|
|
908
|
+
entries[key] = value;
|
|
909
|
+
}
|
|
910
|
+
return entries;
|
|
911
|
+
}
|
|
912
|
+
function pathEscapesRoot(root, path) {
|
|
913
|
+
const child = relative(root, path);
|
|
914
|
+
return child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child);
|
|
915
|
+
}
|
|
916
|
+
function androidAvdConfigSetting(settings, settingsRoot) {
|
|
917
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.android)) return {};
|
|
918
|
+
const android = settings.android;
|
|
919
|
+
let entries = {};
|
|
920
|
+
if ("avdConfigFile" in android) {
|
|
921
|
+
if (typeof android.avdConfigFile !== "string" || !android.avdConfigFile.trim() || android.avdConfigFile !== android.avdConfigFile.trim() || /[\r\n\0]/.test(android.avdConfigFile) || isAbsolute(android.avdConfigFile)) throw new Error("Invalid android.avdConfigFile setting. Expected a relative file path inside the app directory.");
|
|
922
|
+
try {
|
|
923
|
+
const root = realpathSync$1(settingsRoot);
|
|
924
|
+
const candidate = resolve(root, android.avdConfigFile);
|
|
925
|
+
if (pathEscapesRoot(root, candidate)) throw new Error("path escapes the settings root");
|
|
926
|
+
const path = realpathSync$1(candidate);
|
|
927
|
+
if (pathEscapesRoot(root, path)) throw new Error("symlink target escapes the settings root");
|
|
928
|
+
const stat = statSync$1(path);
|
|
929
|
+
if (!stat.isFile()) throw new Error("path is not a regular file");
|
|
930
|
+
if (stat.size > 65536) throw new Error("file exceeds 64 KiB");
|
|
931
|
+
const contents = readFileSync$1(path, "utf8");
|
|
932
|
+
if (Buffer.byteLength(contents) > 65536) throw new Error("file exceeds 64 KiB");
|
|
933
|
+
entries = parseAndroidAvdConfigIni(contents);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
throw new Error(`Could not read valid android.avdConfigFile ${android.avdConfigFile}: ${String(error?.message || error)}`, { cause: error });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if ("avdConfig" in android) {
|
|
939
|
+
if (!isPlainObject(android.avdConfig)) throw new Error("Invalid android.avdConfig setting. Expected an object of native AVD key/value pairs.");
|
|
940
|
+
for (const [key, value] of Object.entries(android.avdConfig)) {
|
|
941
|
+
const [normalizedKey, normalizedValue] = normalizedAndroidAvdConfigEntry(key, value);
|
|
942
|
+
entries[normalizedKey] = normalizedValue;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return entries;
|
|
946
|
+
}
|
|
947
|
+
function androidAvdConfigSettingError(settings, projectPath) {
|
|
948
|
+
try {
|
|
949
|
+
androidAvdConfigSetting(settings, projectPath);
|
|
950
|
+
return null;
|
|
951
|
+
} catch (error) {
|
|
952
|
+
return String(error?.message || error);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function iosSimSlimProfileSetting(settings, settingsRoot) {
|
|
956
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.ios) || !("simslimProfile" in settings.ios)) return null;
|
|
957
|
+
const value = settings.ios.simslimProfile;
|
|
958
|
+
if (typeof value !== "string" || !value.trim() || value !== value.trim() || /[\r\n\0]/.test(value) || isAbsolute(value)) throw new Error("Invalid ios.simslimProfile setting. Expected a relative JSON file path inside the app directory.");
|
|
959
|
+
try {
|
|
960
|
+
const root = realpathSync$1(settingsRoot);
|
|
961
|
+
const candidate = resolve(root, value);
|
|
962
|
+
if (pathEscapesRoot(root, candidate)) throw new Error("path escapes the settings root");
|
|
963
|
+
const path = realpathSync$1(candidate);
|
|
964
|
+
if (pathEscapesRoot(root, path)) throw new Error("symlink target escapes the settings root");
|
|
965
|
+
const stat = statSync$1(path);
|
|
966
|
+
if (!stat.isFile()) throw new Error("path is not a regular file");
|
|
967
|
+
if (stat.size > 65536) throw new Error("file exceeds 64 KiB");
|
|
968
|
+
return path;
|
|
969
|
+
} catch (error) {
|
|
970
|
+
throw new Error(`Could not read ios.simslimProfile ${value}: ${String(error?.message || error)}`, { cause: error });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function iosString(settings, key) {
|
|
974
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.ios)) return void 0;
|
|
975
|
+
return settings.ios[key];
|
|
976
|
+
}
|
|
977
|
+
function iosSigningIdentitySetting(settings) {
|
|
978
|
+
const raw = iosString(settings, "signingIdentity");
|
|
979
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
980
|
+
}
|
|
981
|
+
function iosSigningIdentitySettingError(settings) {
|
|
982
|
+
const raw = iosString(settings, "signingIdentity");
|
|
983
|
+
if (raw === void 0) return null;
|
|
984
|
+
if (typeof raw !== "string" || raw.trim() === "" || /[\r\n\0]/.test(raw)) return `Invalid ios.signingIdentity setting ${JSON.stringify(raw)}. Expected the identity name \`security find-identity -v -p codesigning\` prints, such as "Apple Development: Jane (TEAMID5678)".`;
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
const SIGNING_IDENTITY_SHA1 = /^[0-9A-Fa-f]{40}$/;
|
|
988
|
+
function iosSigningIdentitySha1Setting(settings) {
|
|
989
|
+
const raw = iosString(settings, "signingIdentitySha1");
|
|
990
|
+
return typeof raw === "string" && SIGNING_IDENTITY_SHA1.test(raw.trim()) ? raw.trim().toUpperCase() : null;
|
|
991
|
+
}
|
|
992
|
+
function iosSigningIdentitySha1SettingError(settings) {
|
|
993
|
+
const raw = iosString(settings, "signingIdentitySha1");
|
|
994
|
+
if (raw === void 0) return null;
|
|
995
|
+
if (typeof raw !== "string" || !SIGNING_IDENTITY_SHA1.test(raw.trim())) return `Invalid ios.signingIdentitySha1 setting ${JSON.stringify(raw)}. Expected the 40-character hex SHA-1 hash \`security find-identity -v -p codesigning\` prints beside the identity name.`;
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
const LAN_HOST = /^(?!-)[A-Za-z0-9-]+(?:\.(?!-)[A-Za-z0-9-]+)*$/;
|
|
999
|
+
function iosLanHostSetting(settings) {
|
|
1000
|
+
const raw = iosString(settings, "lanHost");
|
|
1001
|
+
return typeof raw === "string" && LAN_HOST.test(raw.trim()) ? raw.trim() : null;
|
|
1002
|
+
}
|
|
1003
|
+
function iosLanHostSettingError(settings) {
|
|
1004
|
+
const raw = iosString(settings, "lanHost");
|
|
1005
|
+
if (raw === void 0) return null;
|
|
1006
|
+
if (typeof raw !== "string" || !LAN_HOST.test(raw.trim())) return `Invalid ios.lanHost setting ${JSON.stringify(raw)}. Expected a bare address or hostname the phone can reach, such as "192.168.1.42" -- never a URL, a scheme, or a port.`;
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
function unknownSettingKeys(settings, prefix = "") {
|
|
1010
|
+
if (!isPlainObject(settings)) return [];
|
|
1011
|
+
const unknown = [];
|
|
1012
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
1013
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
1014
|
+
if (KNOWN_SETTINGS.has(path) && ![...KNOWN_SETTINGS].some((k) => k.startsWith(`${path}.`))) continue;
|
|
1015
|
+
if (isPlainObject(value) && [...KNOWN_SETTINGS].some((k) => k.startsWith(`${path}.`))) {
|
|
1016
|
+
unknown.push(...unknownSettingKeys(value, path));
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
unknown.push(path);
|
|
1020
|
+
}
|
|
1021
|
+
return unknown;
|
|
1022
|
+
}
|
|
1023
|
+
function readCommittedSettings(directory) {
|
|
1024
|
+
if (!directory) return {};
|
|
1025
|
+
const p = join(directory, ".stim.json");
|
|
1026
|
+
if (!existsSync$1(p)) return {};
|
|
1027
|
+
try {
|
|
1028
|
+
const parsed = JSON.parse(readFileSync$1(p, "utf-8"));
|
|
1029
|
+
return isPlainObject(parsed) ? parsed : {};
|
|
1030
|
+
} catch {
|
|
1031
|
+
return {};
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function resolveSettings({ projectPath, gitCommonDir, repoRoot }) {
|
|
1035
|
+
const machine = loadConfig();
|
|
1036
|
+
return mergeSettingsLayers([
|
|
1037
|
+
projectPath ? getProjectSettings(projectPath) : null,
|
|
1038
|
+
gitCommonDir ? getRepoSettings(gitCommonDir) : null,
|
|
1039
|
+
readCommittedSettings(projectPath ?? repoRoot),
|
|
1040
|
+
machine?.optimizations === void 0 ? null : { optimizations: machine.optimizations }
|
|
1041
|
+
]);
|
|
1042
|
+
}
|
|
1043
|
+
function settingsForProject(root) {
|
|
1044
|
+
return resolveSettings({
|
|
1045
|
+
projectPath: root,
|
|
1046
|
+
gitCommonDir: gitCommonDir(root),
|
|
1047
|
+
repoRoot: repoRoot(root) ?? root
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
function projectOptimizations(root) {
|
|
1051
|
+
return resolveOptimizations(settingsForProject(root));
|
|
1052
|
+
}
|
|
1053
|
+
function projectMetroSharedCache(root) {
|
|
1054
|
+
return resolveMetroSharedCache(settingsForProject(root));
|
|
1055
|
+
}
|
|
1056
|
+
function cacheBlock(layer) {
|
|
1057
|
+
if (!isPlainObject(layer) || !isPlainObject(layer.cache)) return null;
|
|
1058
|
+
return layer.cache;
|
|
1059
|
+
}
|
|
1060
|
+
function resolveCacheProviderConfig({ projectPath, gitCommonDir, repoRoot }) {
|
|
1061
|
+
const layers = [
|
|
1062
|
+
{
|
|
1063
|
+
settings: projectPath ? getProjectSettings(projectPath) : {},
|
|
1064
|
+
baseDir: projectPath ?? null
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
settings: gitCommonDir ? getRepoSettings(gitCommonDir) : {},
|
|
1068
|
+
baseDir: repoRoot ?? projectPath ?? null
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
settings: readCommittedSettings(projectPath),
|
|
1072
|
+
baseDir: projectPath ?? null
|
|
1073
|
+
}
|
|
1074
|
+
];
|
|
1075
|
+
let provider = null;
|
|
1076
|
+
let baseDir = null;
|
|
1077
|
+
for (const layer of layers) {
|
|
1078
|
+
const reference = cacheBlock(layer.settings)?.provider;
|
|
1079
|
+
if (typeof reference !== "string" || reference.trim() === "" || layer.baseDir === null) continue;
|
|
1080
|
+
provider = reference.trim();
|
|
1081
|
+
baseDir = layer.baseDir;
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
if (provider === null || baseDir === null) return null;
|
|
1085
|
+
const options = mergeSettingsLayers(layers.map((layer) => {
|
|
1086
|
+
const block = cacheBlock(layer.settings)?.options;
|
|
1087
|
+
return isPlainObject(block) ? block : null;
|
|
1088
|
+
}));
|
|
1089
|
+
return {
|
|
1090
|
+
provider,
|
|
1091
|
+
options,
|
|
1092
|
+
baseDir
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function cacheProviderSettingError(settings) {
|
|
1096
|
+
const block = settings.cache;
|
|
1097
|
+
if (!isPlainObject(block)) return block === void 0 ? null : `Invalid cache setting ${JSON.stringify(block)}. Expected an object with provider and options.`;
|
|
1098
|
+
if ("provider" in block && (typeof block.provider !== "string" || block.provider.trim() === "")) return `Invalid cache.provider setting ${JSON.stringify(block.provider)}. Expected a module path or package name.`;
|
|
1099
|
+
if ("options" in block && !isPlainObject(block.options)) return `Invalid cache.options setting ${JSON.stringify(block.options)}. Expected an object.`;
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
const REMOTE_DEVICE_BACKENDS = ["proxy", "eas"];
|
|
1103
|
+
function remoteIosSetting(settings) {
|
|
1104
|
+
return remoteSetting(settings, "ios");
|
|
1105
|
+
}
|
|
1106
|
+
function remoteAndroidSetting(settings) {
|
|
1107
|
+
return remoteSetting(settings, "android");
|
|
1108
|
+
}
|
|
1109
|
+
function remoteSetting(settings, platform) {
|
|
1110
|
+
const block = settings[platform];
|
|
1111
|
+
if (!isPlainObject(block)) return null;
|
|
1112
|
+
const remote = block.remote;
|
|
1113
|
+
return typeof remote === "string" && REMOTE_DEVICE_BACKENDS.includes(remote) ? remote : null;
|
|
1114
|
+
}
|
|
1115
|
+
function remoteDeviceSettingError(settings) {
|
|
1116
|
+
for (const platform of ["ios", "android"]) {
|
|
1117
|
+
const block = settings[platform];
|
|
1118
|
+
if (!isPlainObject(block) || !("remote" in block)) continue;
|
|
1119
|
+
if (remoteSetting(settings, platform) === null) return `Invalid ${platform}.remote setting ${JSON.stringify(block.remote)}. Expected one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}.`;
|
|
1120
|
+
}
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
function tunnelModeSetting(settings) {
|
|
1124
|
+
const block = settings.metro;
|
|
1125
|
+
if (typeof block !== "object" || block === null) return null;
|
|
1126
|
+
const mode = block.tunnel;
|
|
1127
|
+
return typeof mode === "string" && TUNNEL_MODES.includes(mode) ? mode : null;
|
|
1128
|
+
}
|
|
1129
|
+
function metroTunnelSettingError(settings) {
|
|
1130
|
+
const block = settings.metro;
|
|
1131
|
+
if (!isPlainObject(block)) return null;
|
|
1132
|
+
if ("tunnel" in block) {
|
|
1133
|
+
const mode = block.tunnel;
|
|
1134
|
+
if (typeof mode !== "string" || !TUNNEL_MODES.includes(mode)) return `Invalid metro.tunnel setting ${JSON.stringify(mode)}. Expected one of: ${TUNNEL_MODES.join(", ")}.`;
|
|
1135
|
+
}
|
|
1136
|
+
if (!("ngrokUrl" in block)) return null;
|
|
1137
|
+
if (block.tunnel !== "ngrok") return "metro.ngrokUrl requires metro.tunnel to be \"ngrok\".";
|
|
1138
|
+
if (normalizedHttpsUrl(block.ngrokUrl) === null) return "metro.ngrokUrl must be a valid HTTPS URL.";
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
function publicUrlSetting(settings) {
|
|
1142
|
+
const block = settings.metro;
|
|
1143
|
+
if (typeof block !== "object" || block === null) return null;
|
|
1144
|
+
const url = block.publicUrl;
|
|
1145
|
+
return typeof url === "string" && url.trim() ? url : null;
|
|
1146
|
+
}
|
|
1147
|
+
function ngrokUrlSetting(settings) {
|
|
1148
|
+
const block = settings.metro;
|
|
1149
|
+
if (typeof block !== "object" || block === null) return null;
|
|
1150
|
+
if (block.tunnel !== "ngrok") return null;
|
|
1151
|
+
return normalizedHttpsUrl(block.ngrokUrl);
|
|
1152
|
+
}
|
|
1153
|
+
function normalizedHttpsUrl(raw) {
|
|
1154
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
1155
|
+
try {
|
|
1156
|
+
return new URL(raw.trim()).protocol === "https:" ? raw.trim().replace(/\/+$/, "") : null;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/supervisor/metro-store.ts
|
|
1163
|
+
function metroStoreName(root) {
|
|
1164
|
+
try {
|
|
1165
|
+
const name = JSON.parse(readFileSync(join$1(root, "package.json"), "utf-8"))?.name;
|
|
1166
|
+
if (typeof name === "string" && name.trim() !== "") return name;
|
|
1167
|
+
} catch {}
|
|
1168
|
+
return "app";
|
|
1169
|
+
}
|
|
1170
|
+
function metroStoreRoot(root) {
|
|
1171
|
+
return metroCacheRoot(metroStoreName(root));
|
|
1172
|
+
}
|
|
1173
|
+
function registerMetroStore(storeRoot) {
|
|
1174
|
+
const parent = resolve$1(metroCacheRoot());
|
|
1175
|
+
const dir = resolve$1(storeRoot);
|
|
1176
|
+
registerCache({
|
|
1177
|
+
dir,
|
|
1178
|
+
name: "Metro transform cache",
|
|
1179
|
+
prune: "entries",
|
|
1180
|
+
entriesDepth: 2,
|
|
1181
|
+
layout: METRO_NAMED_CACHE_LAYOUT,
|
|
1182
|
+
note: "shared Metro transforms, installed by stim start; no eviction of its own",
|
|
1183
|
+
replaces: dir === parent ? [] : [{
|
|
1184
|
+
dir: parent,
|
|
1185
|
+
name: "Metro transform cache",
|
|
1186
|
+
prune: "entries",
|
|
1187
|
+
entriesDepth: 2,
|
|
1188
|
+
layout: null
|
|
1189
|
+
}]
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
function hasStoreAt(stores, storeRoot) {
|
|
1193
|
+
if (!Array.isArray(stores)) return false;
|
|
1194
|
+
return stores.some((s) => sharedStoreRoot(s) === storeRoot);
|
|
1195
|
+
}
|
|
1196
|
+
function appendCacheStore(config, { storeRoot, FileStore }) {
|
|
1197
|
+
if (!config || typeof config !== "object") return {
|
|
1198
|
+
added: false,
|
|
1199
|
+
storeRoot,
|
|
1200
|
+
reason: "no config to add it to"
|
|
1201
|
+
};
|
|
1202
|
+
const stores = config.cacheStores;
|
|
1203
|
+
if (typeof stores === "function") {
|
|
1204
|
+
const original = stores;
|
|
1205
|
+
config.cacheStores = (cache) => {
|
|
1206
|
+
const resolved = original(cache);
|
|
1207
|
+
return (Array.isArray(resolved) ? resolved : []).concat([tagSharedStore(new FileStore({ root: storeRoot }), storeRoot)]);
|
|
1208
|
+
};
|
|
1209
|
+
return {
|
|
1210
|
+
added: true,
|
|
1211
|
+
storeRoot
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
if (hasStoreAt(stores, storeRoot)) return {
|
|
1215
|
+
added: false,
|
|
1216
|
+
storeRoot,
|
|
1217
|
+
reason: "a store already points at it"
|
|
1218
|
+
};
|
|
1219
|
+
config.cacheStores = [...Array.isArray(stores) ? stores : [], tagSharedStore(new FileStore({ root: storeRoot }), storeRoot)];
|
|
1220
|
+
return {
|
|
1221
|
+
added: true,
|
|
1222
|
+
storeRoot
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
const EXPO_ADAPTER_FILE = "expo-metro-config.cjs";
|
|
1226
|
+
function expoMetroConfigPath(fromUrl = import.meta.url) {
|
|
1227
|
+
const here = dirname$1(fileURLToPath(fromUrl));
|
|
1228
|
+
for (const rel of ["../shim", "../../shim"]) {
|
|
1229
|
+
const candidate = resolve$1(here, rel, EXPO_ADAPTER_FILE);
|
|
1230
|
+
if (existsSync(candidate)) return candidate;
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
const STORE_OK_PREFIX = "stim-metro-store: sharing Metro transforms through ";
|
|
1235
|
+
function metroStoreConfirmedRoot(line) {
|
|
1236
|
+
if (!line.startsWith(STORE_OK_PREFIX)) return null;
|
|
1237
|
+
const root = line.slice(51).trim();
|
|
1238
|
+
return root === "" ? null : root;
|
|
1239
|
+
}
|
|
1240
|
+
function expoMetroStoreEnv({ root, storeRoot, adapterPath, existingOverride }) {
|
|
1241
|
+
return {
|
|
1242
|
+
EXPO_OVERRIDE_METRO_CONFIG: adapterPath,
|
|
1243
|
+
STIM_METRO_STORE: storeRoot,
|
|
1244
|
+
STIM_PROJECT_ROOT: root,
|
|
1245
|
+
...existingOverride ? { STIM_EXPO_METRO_CONFIG: existingOverride } : {}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
//#endregion
|
|
1249
|
+
export { listWorktrees as $, remoteIosSetting as A, optimizationBuildProfile as B, metroTunnelSettingError as C, publicUrlSetting as D, projectOptimizations as E, unknownSettingKeys as F, depsOutOfSync as G, branchExists as H, PUBLIC_METRO_ENV as I, gitCommonDir as J, dirtyFingerprintFiles as K, detectProviders as L, resolveSettings as M, settingShapeErrors as N, remoteAndroidSetting as O, tunnelModeSetting as P, isPodInstallChurn as Q, planMetroReach as R, iosSimSlimProfileSetting as S, projectMetroSharedCache as T, cloneIgnoredEntries as U, resolveOptimizations as V, deleteBranch as W, hasUncommittedWork as X, hasRemote as Y, isMainWorkingTree as Z, iosLanHostSettingError as _, metroStoreRoot as a, restoreFile as at, iosSigningIdentitySha1Setting as b, REMOTE_DEVICE_BACKENDS as c, filesystemDevice as ct, androidAvdConfigSettingError as d, temporaryRoot as dt, podsOutOfSync as et, androidDataPartitionSizeBytes as f, iosLanHostSetting as g, cacheProviderSettingError as h, metroStoreConfirmedRoot as i, resolveFullRef as it, resolveCacheProviderConfig as j, remoteDeviceSettingError as k, SETTING_SHAPE_REMEDY as l, makeTemporaryDirectory as lt, androidDataPartitionSizeGbSettingError as m, expoMetroConfigPath as n, removeWorktree as nt, registerMetroStore as o, unpushedCommits as ot, androidDataPartitionSizeGbSetting as p, dirtyPaths as q, expoMetroStoreEnv as r, repoRoot as rt, ANDROID_AVD_CONFIG_HELP as s, warmWorktreePaths as st, appendCacheStore as t, readWorktreeExclude as tt, androidAvdConfigSetting as u, removeTemporaryEntry as ut, iosSigningIdentitySetting as v, ngrokUrlSetting as w, iosSigningIdentitySha1SettingError as x, iosSigningIdentitySettingError as y, artifactCachePolicy as z };
|