@stim-cli/core 1.8.0-rc.1 → 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.
package/dist/index.d.mts CHANGED
@@ -30,6 +30,10 @@ declare function workspaceId(projectRoot: string): string;
30
30
  declare function workspaceName(projectRoot: string): string;
31
31
  declare function workspaceStateDir(projectRoot: string): string;
32
32
  declare function workspaceLogDir(projectRoot: string): string;
33
+ declare const LOG_ROTATE_BYTES: number;
34
+ declare function rotatedLogPath(file: string): string;
35
+ /** Moves `file` over its previous generation once it reaches `maxBytes`; readers read both generations. */
36
+ declare function rotateLog(file: string, maxBytes: number): void;
33
37
  declare function cachePathSetting(key: "buildCache" | "metroCache"): string | null;
34
38
  declare function cacheNameSegment(name: string | null | undefined): string;
35
39
  declare function buildCacheRoot(): string;
@@ -42,6 +46,7 @@ interface BuildRunOptions {
42
46
  compiler?: string;
43
47
  variant?: string;
44
48
  abi?: string;
49
+ allArch?: boolean;
45
50
  configuration?: string;
46
51
  scheme?: string;
47
52
  buildConfiguration?: string;
@@ -75,4 +80,4 @@ declare function readCacheManifest(file: string): CacheManifest;
75
80
  declare function updateCacheManifest(file: string, mutate: (caches: Array<Record<string, unknown>>) => Array<Record<string, unknown>>): CacheManifest;
76
81
  declare function registerCache({ dir, name, prune, note, entriesDepth, layout, replaces }: RegisterOptions): void;
77
82
  //#endregion
78
- export { BuildRunOptions, CacheManifest, CacheRegistrationMatch, type DirLockOptions, METRO_NAMED_CACHE_LAYOUT, RegisterOptions, STORE_ROOT_TAG, type StoreArtifactOptions, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
83
+ export { BuildRunOptions, CacheManifest, CacheRegistrationMatch, type DirLockOptions, LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, RegisterOptions, STORE_ROOT_TAG, type StoreArtifactOptions, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, rotateLog, rotatedLogPath, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-Cn3NHqBS.mjs";
1
+ import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
2
2
  import fs, { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path, { basename, dirname, join } from "node:path";
@@ -165,43 +165,94 @@ function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetad
165
165
  const existing = artifactIn(dest);
166
166
  if (existing && !overwrite) return existing;
167
167
  const staging = `${dest}.staging-${process.pid}`;
168
+ const replaced = `${dest}.replaced-${process.pid}`;
169
+ const target = join(staging, basename(buildPath));
168
170
  rmSync(staging, {
169
171
  recursive: true,
170
172
  force: true
171
173
  });
172
174
  mkdirSync(staging, { recursive: true });
173
175
  try {
174
- runFile("cp", [
175
- "-c",
176
- "-R",
177
- buildPath,
178
- join(staging, basename(buildPath))
179
- ]);
180
- } catch {
181
- runFile("cp", [
182
- "-R",
183
- buildPath,
184
- join(staging, basename(buildPath))
185
- ]);
176
+ try {
177
+ runFile("cp", [
178
+ "-c",
179
+ "-R",
180
+ buildPath,
181
+ target
182
+ ]);
183
+ } catch {
184
+ rmSync(target, {
185
+ recursive: true,
186
+ force: true
187
+ });
188
+ runFile("cp", [
189
+ "-R",
190
+ buildPath,
191
+ target
192
+ ]);
193
+ }
194
+ writeMetadata?.(staging);
195
+ const published = artifactIn(dest);
196
+ if (published && !overwrite) {
197
+ rmSync(staging, {
198
+ recursive: true,
199
+ force: true
200
+ });
201
+ return published;
202
+ }
203
+ mkdirSync(dirname(dest), { recursive: true });
204
+ rmSync(replaced, {
205
+ recursive: true,
206
+ force: true
207
+ });
208
+ if (existsSync(dest)) renameSync(dest, replaced);
209
+ } catch (error) {
210
+ rmSync(staging, {
211
+ recursive: true,
212
+ force: true
213
+ });
214
+ throw error;
186
215
  }
187
- writeMetadata?.(staging);
188
- mkdirSync(dirname(dest), { recursive: true });
189
- rmSync(dest, {
190
- recursive: true,
191
- force: true
192
- });
193
216
  try {
194
217
  renameSync(staging, dest);
195
218
  } catch (error) {
196
- if (!onRenameError) throw error;
219
+ try {
220
+ renameSync(replaced, dest);
221
+ } catch {}
222
+ if (!onRenameError) {
223
+ rmSync(staging, {
224
+ recursive: true,
225
+ force: true
226
+ });
227
+ rmSync(replaced, {
228
+ recursive: true,
229
+ force: true
230
+ });
231
+ throw error;
232
+ }
197
233
  onRenameError(staging);
198
234
  }
235
+ rmSync(replaced, {
236
+ recursive: true,
237
+ force: true
238
+ });
199
239
  return artifactIn(dest);
200
240
  }
201
241
  //#endregion
202
242
  //#region index.ts
243
+ const warnedRelative = /* @__PURE__ */ new Set();
244
+ function absoluteEnvPath(name) {
245
+ const value = process.env[name];
246
+ if (!value) return null;
247
+ if (path.isAbsolute(value)) return value;
248
+ if (!warnedRelative.has(name)) {
249
+ warnedRelative.add(name);
250
+ process.emitWarning(`${name}=${value} is not an absolute path and is ignored.`, { code: "STIM_RELATIVE_PATH" });
251
+ }
252
+ return null;
253
+ }
203
254
  function configDir() {
204
- return process.env.STIM_HOME || path.join(os.homedir(), ".stim");
255
+ return absoluteEnvPath("STIM_HOME") || path.join(os.homedir(), ".stim");
205
256
  }
206
257
  function workspaceSlug(projectRoot) {
207
258
  return path.basename(path.resolve(projectRoot)).normalize("NFKD").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase().slice(0, 48) || "workspace";
@@ -218,6 +269,27 @@ function workspaceStateDir(projectRoot) {
218
269
  function workspaceLogDir(projectRoot) {
219
270
  return path.join(workspaceStateDir(projectRoot), "logs");
220
271
  }
272
+ const LOG_ROTATE_BYTES = 8388608;
273
+ function rotatedLogPath(file) {
274
+ return `${file}.1`;
275
+ }
276
+ /** Moves `file` over its previous generation once it reaches `maxBytes`; readers read both generations. */
277
+ function rotateLog(file, maxBytes) {
278
+ if (logSize(file) < maxBytes) return;
279
+ withDirLock(`${file}.lock`, () => {
280
+ if (logSize(file) >= maxBytes) fs.renameSync(file, rotatedLogPath(file));
281
+ }, {
282
+ waitMs: 100,
283
+ pollMs: 10
284
+ });
285
+ }
286
+ function logSize(file) {
287
+ try {
288
+ return fs.statSync(file).size;
289
+ } catch {
290
+ return 0;
291
+ }
292
+ }
221
293
  function cachePathSetting(key) {
222
294
  try {
223
295
  const value = JSON.parse(fs.readFileSync(path.join(configDir(), "config.json"), "utf-8"))?.caches?.[key];
@@ -230,10 +302,10 @@ function cacheNameSegment(name) {
230
302
  return String(name).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "app";
231
303
  }
232
304
  function buildCacheRoot() {
233
- return process.env.STIM_BUILD_CACHE || cachePathSetting("buildCache") || path.join(configDir(), "build-cache");
305
+ return absoluteEnvPath("STIM_BUILD_CACHE") || cachePathSetting("buildCache") || path.join(configDir(), "build-cache");
234
306
  }
235
307
  function metroCacheRoot(name) {
236
- const root = process.env.STIM_METRO_CACHE || cachePathSetting("metroCache") || path.join(configDir(), "metro-cache");
308
+ const root = absoluteEnvPath("STIM_METRO_CACHE") || cachePathSetting("metroCache") || path.join(configDir(), "metro-cache");
237
309
  return name === void 0 || name === null || name === "" ? root : path.join(root, cacheNameSegment(name));
238
310
  }
239
311
  const STORE_ROOT_TAG = "stimStoreRoot";
@@ -276,10 +348,11 @@ function buildTarget(options) {
276
348
  function buildCacheKey(platform, fingerprintHash, options = {}) {
277
349
  const opts = options && typeof options === "object" ? options : {};
278
350
  const abi = platform === "android" && typeof opts.abi === "string" ? slug(opts.abi) : "";
351
+ const allArch = platform === "android" && opts.allArch === true ? "-all-arch" : "";
279
352
  const compiler = typeof opts.compiler === "string" ? slug(opts.compiler) : "";
280
353
  const profile = typeof opts.buildProfile === "string" ? slug(opts.buildProfile) : "";
281
354
  const scheme = platform === "ios" && typeof opts.scheme === "string" && opts.scheme ? createHash("sha256").update(opts.scheme).digest("hex") : "";
282
- return `${fingerprintHash}-${buildVariant(platform, opts)}-${buildTarget(opts)}${abi ? `-${abi}` : ""}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
355
+ return `${fingerprintHash}-${buildVariant(platform, opts)}-${buildTarget(opts)}${abi ? `-${abi}` : ""}${allArch}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
283
356
  }
284
357
  const METRO_NAMED_CACHE_LAYOUT = "metro-named-v1";
285
358
  function cacheManifestLockPath(file) {
@@ -318,6 +391,7 @@ function updateCacheManifest(file, mutate) {
318
391
  }, { ensureParent: () => fs.mkdirSync(parent, { recursive: true }) });
319
392
  }
320
393
  function registerCache({ dir, name, prune, note, entriesDepth, layout, replaces = [] }) {
394
+ if (!path.isAbsolute(dir)) return;
321
395
  try {
322
396
  updateCacheManifest(path.join(configDir(), "caches.json"), (caches) => {
323
397
  const others = caches.filter((cache) => cache.dir !== dir && !replaces.some((match) => matchesCache(cache, match)));
@@ -344,4 +418,4 @@ function matchesCache(cache, match) {
344
418
  return match.layout === void 0 || cache.layout === match.layout;
345
419
  }
346
420
  //#endregion
347
- export { METRO_NAMED_CACHE_LAYOUT, STORE_ROOT_TAG, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
421
+ export { LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, STORE_ROOT_TAG, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, rotateLog, rotatedLogPath, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
@@ -18,7 +18,7 @@ const CLAIM_SUFFIX = ".claim";
18
18
  const CHILD_SUFFIX = ".child";
19
19
  const STAGING_PREFIX = ".staging-";
20
20
  const PUBLISH_ATTEMPTS = 64;
21
- const WIN32_SETTLE_MS = 250;
21
+ const WIN32_SETTLE_MS = 2e3;
22
22
  const WIN32_SETTLE_STEP_MS = 5;
23
23
  const debug = debuglog("stim:claim");
24
24
  var ClaimRefusedError = class extends Error {
@@ -99,61 +99,71 @@ function sleepSync(ms) {
99
99
  * Run a read, listing or removal and resolve the one answer Windows leaves ambiguous. NTFS answers
100
100
  * ERROR_ACCESS_DENIED, which Node reports as EPERM, for a name whose removal by another process is still
101
101
  * in flight, and for every name below it, where POSIX answers ENOENT; measured to settle within 134 ms
102
- * with 12 processes on 4 cores (https://github.com/appandflow/stim/issues/883). The call is reissued in
103
- * small steps for at most WIN32_SETTLE_MS: a result or ENOENT is the settled answer, and an EPERM that
104
- * outlasts the window is a real denial, thrown exactly as a POSIX EACCES is. This bounds the resolution
105
- * of an ambiguous OS answer; it never waits on a holder.
102
+ * with 12 processes on 4 cores (https://github.com/appandflow/stim/issues/883), and within 986 ms on an
103
+ * oversubscribed hosted windows-latest runner (https://github.com/appandflow/stim/issues/943). The call
104
+ * is reissued in small steps until a call issued WIN32_SETTLE_MS after the first EPERM still answers
105
+ * EPERM, so a call that was itself descheduled past the window never ends it: a result or ENOENT is the
106
+ * settled answer, and an EPERM that outlasts the window is a real denial, thrown exactly as a POSIX
107
+ * EACCES is. This bounds the resolution of an ambiguous OS answer; it never waits on a holder.
106
108
  */
107
109
  function settledAnswer(path, call) {
108
110
  if (process.platform !== "win32") return call();
109
- const started = Date.now();
111
+ let denied;
110
112
  for (;;) {
113
+ const asked = Date.now();
111
114
  try {
112
115
  const result = call();
113
- if (Date.now() > started) debug("%s settled after %d ms", path, Date.now() - started);
116
+ if (denied !== void 0) debug("%s settled after %d ms", path, Date.now() - denied);
114
117
  return result;
115
118
  } catch (err) {
116
- const code = err?.code;
117
- if (code !== "EPERM" || Date.now() - started >= WIN32_SETTLE_MS) {
118
- if (code === "EPERM") debug("%s still answers EPERM after %d ms", path, Date.now() - started);
119
+ if (err?.code !== "EPERM") throw err;
120
+ denied ??= Date.now();
121
+ if (asked - denied >= WIN32_SETTLE_MS) {
122
+ debug("%s still answers EPERM after %d ms", path, Date.now() - denied);
119
123
  throw err;
120
124
  }
121
125
  }
122
126
  sleepSync(WIN32_SETTLE_STEP_MS);
123
127
  }
124
128
  }
125
- function readChild(claimPath) {
126
- let text;
129
+ var Unreadable = class {
130
+ reason;
131
+ constructor(reason) {
132
+ this.reason = reason;
133
+ }
134
+ };
135
+ const NOT_JSON = new Unreadable("its record is missing, truncated or not valid JSON");
136
+ function readRecord(path, what) {
127
137
  try {
128
- text = settledAnswer(childPath(claimPath), () => readFileSync(childPath(claimPath), "utf-8"));
138
+ return settledAnswer(path, () => readFileSync(path, "utf-8"));
129
139
  } catch (err) {
130
- if (err?.code === "ENOENT") return null;
131
- return "unreadable";
140
+ const code = err?.code;
141
+ if (code === "ENOENT") return null;
142
+ return new Unreadable(`its ${what} could not be read (${code || err?.message})`);
132
143
  }
144
+ }
145
+ function readChild(claimPath) {
146
+ const text = readRecord(childPath(claimPath), "child record");
147
+ if (text === null || text instanceof Unreadable) return text;
133
148
  try {
134
149
  return { record: asOwner(JSON.parse(text)?.record) };
135
150
  } catch {
136
- return "unreadable";
151
+ return NOT_JSON;
137
152
  }
138
153
  }
139
154
  function readClaim(path, mode) {
140
- let text;
141
- try {
142
- text = settledAnswer(path, () => readFileSync(path, "utf-8"));
143
- } catch (err) {
144
- if (err?.code === "ENOENT") return null;
145
- return "unreadable";
146
- }
155
+ const text = readRecord(path, "record");
156
+ if (text === null || text instanceof Unreadable) return text;
147
157
  let parsed;
148
158
  try {
149
159
  parsed = JSON.parse(text);
150
160
  } catch {
151
- return "unreadable";
161
+ return NOT_JSON;
152
162
  }
153
163
  const owner = asOwner(parsed?.owner);
154
- if (!owner || typeof parsed.claimId !== "string" || !parsed.claimId) return "unreadable";
164
+ if (!owner || typeof parsed.claimId !== "string" || !parsed.claimId) return NOT_JSON;
155
165
  const child = readChild(path);
156
- if (child === "unreadable") return "unreadable";
166
+ if (child instanceof Unreadable) return child;
157
167
  return {
158
168
  path,
159
169
  claimId: parsed.claimId,
@@ -183,7 +193,7 @@ function processGroupAlive(pid) {
183
193
  function childLiveness(claimPath) {
184
194
  const child = readChild(claimPath);
185
195
  if (child === null) return "none";
186
- if (child === "unreadable" || !child.record) return "unknown";
196
+ if (child instanceof Unreadable || !child.record) return "unknown";
187
197
  const status = inspectProcessIdentity(child.record);
188
198
  if (status === "same") return "live";
189
199
  if (status === "gone") return processGroupAlive(child.record.pid) ? "live" : "dead";
@@ -252,10 +262,10 @@ function survey(dir, mode, into) {
252
262
  const path = join(dir, name);
253
263
  const holder = readClaim(path, mode);
254
264
  if (holder === null) continue;
255
- if (holder === "unreadable") {
265
+ if (holder instanceof Unreadable) {
256
266
  into.unresolved.push({
257
267
  path,
258
- reason: "its record is missing, truncated or not valid JSON"
268
+ reason: holder.reason
259
269
  });
260
270
  continue;
261
271
  }
@@ -296,7 +306,7 @@ function removeOrRefuse(path, root, claimPath, label) {
296
306
  function reap(holder, root, label) {
297
307
  const again = readClaim(holder.path, holder.mode);
298
308
  if (again === null) return true;
299
- if (again === "unreadable" || again.claimId !== holder.claimId) return false;
309
+ if (again instanceof Unreadable || again.claimId !== holder.claimId) return false;
300
310
  removeOrRefuse(childPath(holder.path), root, holder.path, label);
301
311
  removeOrRefuse(holder.path, root, holder.path, label);
302
312
  return true;
@@ -606,7 +616,7 @@ function releaseClaim(handle) {
606
616
  if (!handle) return false;
607
617
  const current = readClaim(handle.path, handle.mode);
608
618
  if (current === null) return true;
609
- if (current === "unreadable" || current.claimId !== handle.claimId) return false;
619
+ if (current instanceof Unreadable || current.claimId !== handle.claimId) return false;
610
620
  try {
611
621
  rmSync(childPath(handle.path), { force: true });
612
622
  rmSync(handle.path, { force: true });
@@ -1,2 +1,2 @@
1
- import { _ as releaseClaim, a as claimFailure, b as sharedClaimDir, c as clearClaimChild, d as inspectClaimSet, f as isClaimRefusal, g as readClaimSet, h as processGroupAlive, i as ClaimUnavailableError, l as clearFreeClaimSet, m as markClaimChildPending, n as CLAIM_UNAVAILABLE, o as claimLiveness, p as isClaimUnavailable, r as ClaimRefusedError, s as claimRemoveCommand, t as CLAIM_REFUSED, u as exclusiveClaimDir, v as setClaimChild, x as tryAcquireClaim, y as settleClaim } from "./ownership-claim-Cn3NHqBS.mjs";
1
+ import { _ as releaseClaim, a as claimFailure, b as sharedClaimDir, c as clearClaimChild, d as inspectClaimSet, f as isClaimRefusal, g as readClaimSet, h as processGroupAlive, i as ClaimUnavailableError, l as clearFreeClaimSet, m as markClaimChildPending, n as CLAIM_UNAVAILABLE, o as claimLiveness, p as isClaimUnavailable, r as ClaimRefusedError, s as claimRemoveCommand, t as CLAIM_REFUSED, u as exclusiveClaimDir, v as setClaimChild, x as tryAcquireClaim, y as settleClaim } from "./ownership-claim-O31Y5iIb.mjs";
2
2
  export { CLAIM_REFUSED, CLAIM_UNAVAILABLE, ClaimRefusedError, ClaimUnavailableError, claimFailure, claimLiveness, claimRemoveCommand, clearClaimChild, clearFreeClaimSet, exclusiveClaimDir, inspectClaimSet, isClaimRefusal, isClaimUnavailable, markClaimChildPending, processGroupAlive, readClaimSet, releaseClaim, setClaimChild, settleClaim, sharedClaimDir, tryAcquireClaim };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stim-cli/core",
3
- "version": "1.8.0-rc.1",
3
+ "version": "1.9.0",
4
4
  "description": "Shared internal cache and runtime primitives for Stim packages.",
5
5
  "license": "MIT",
6
6
  "repository": {