@vzn/vx-reapi 0.0.643 → 0.0.645

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 vx contributors
3
+ Copyright (c) 2026 VZN
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-reapi",
3
- "version": "0.0.643",
3
+ "version": "0.0.645",
4
4
  "license": "MIT",
5
5
  "description": "Bazel Remote Execution API plugin for vx — remote cache (ActionCache + CAS) against NativeLink, BuildBuddy, Buildbarn or bazel-remote.",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "peerDependencies": {
42
- "@vzn/vx": "^0.0.643"
42
+ "@vzn/vx": "^0.0.645"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",
package/src/cache.ts CHANGED
@@ -96,12 +96,18 @@ export class ReapiRemoteCache {
96
96
  * hit vs miss. Costs one extra round trip, and only for a PREDICTED HIT:
97
97
  * a miss still answers in one call.
98
98
  */
99
- async has(hash: string): Promise<boolean> {
99
+ async has(hash: string): Promise<{ size: number; durationMs?: number } | null> {
100
100
  const result = await this.client.getActionResult(actionDigestFor(hash))
101
- if (result === null) return false
101
+ if (result === null) return null
102
102
  const file = result.output_files?.find((f) => f.path === ARTIFACT_PATH)
103
- if (file === undefined) return false
104
- return (await this.client.findMissingBlobs([file.digest])).length === 0
103
+ if (file === undefined) return null
104
+ if ((await this.client.findMissingBlobs([file.digest])).length > 0) return null
105
+ // The duration only when it rode inline: a CAS read for it is a round trip.
106
+ const durationMs = decodeDuration(result.stdout_raw)
107
+ return {
108
+ size: Number(file.digest.size_bytes),
109
+ ...(durationMs === undefined ? {} : { durationMs }),
110
+ }
105
111
  }
106
112
 
107
113
  /**
package/src/executor.ts CHANGED
@@ -76,8 +76,9 @@ async function decomposeOutputDir(
76
76
  // Only globs that COLLAPSED to this entry are interesting; a literal glob
77
77
  // already names its own path.
78
78
  const wild = globs
79
- .filter((g) => globToOutputPath(g) === entry.path && g !== entry.path)
80
- .map((g) => g.slice(entry.path.length + 1).split('/'))
79
+ .map(literalHead)
80
+ .filter((h) => h.path === entry.path && h.rest.length > 0)
81
+ .map((h) => h.rest)
81
82
  if (wild.length === 0) return whole
82
83
  // Only whole-segment wildcards are walked. One glob the walk cannot follow
83
84
  // (`*.js`, `**`) keeps the entry whole: splitting for its siblings would
@@ -1267,17 +1268,23 @@ function fullCommand(req: ExecuteRequest, cdInto: string, projectRel: string): s
1267
1268
  * a glob whose FIRST segment already has the wildcard collapses to `''` —
1268
1269
  * which REAPI defines as "the entire working directory". Passing the raw
1269
1270
  * glob instead would name a file literally called `dist/**`, and the action
1270
- * would return no outputs with no error anywhere.
1271
+ * would return no outputs with no error anywhere. An escaped segment
1272
+ * (`\[id\]`, a route directory) is literal: the path names it unescaped.
1271
1273
  */
1272
1274
  export function globToOutputPath(glob: string): string {
1275
+ return literalHead(glob).path
1276
+ }
1277
+
1278
+ /** `glob`'s literal head, unescaped, and the segments past it: none when the glob is a literal path. */
1279
+ function literalHead(glob: string): { path: string; rest: string[] } {
1273
1280
  const segments = glob.split('/')
1274
- const literal: string[] = []
1275
- for (const seg of segments) {
1276
- if (!isLiteralPattern(seg)) break
1277
- literal.push(seg)
1278
- }
1279
- if (literal.length === segments.length) return glob // no wildcard: a literal path
1280
- return literal.join('/')
1281
+ let i = 0
1282
+ while (i < segments.length && isLiteralPattern(segments[i]!.replace(/\\./g, '_'))) i++
1283
+ const path = segments
1284
+ .slice(0, i)
1285
+ .map((seg) => seg.replace(/\\(.)/g, '$1'))
1286
+ .join('/')
1287
+ return { path, rest: segments.slice(i) }
1281
1288
  }
1282
1289
 
1283
1290
  /**
@@ -1334,7 +1341,7 @@ export function outputPathSets(
1334
1341
  // PROJECT-relative globs are the ones needing a prefix.
1335
1342
  const prefix = (p: string): string =>
1336
1343
  workingDirectory === '' && projectRel !== '' ? `${projectRel}/${p}` : p
1337
- // Core's spelling first: `app/\[id\]/x` names the file `app/[id]/x` (item 667).
1344
+ // `app/\[id\]/x` names the file `app/[id]/x`: `literalHead` unescapes.
1338
1345
  const globs = [...req.outputs.files.map(prefix), ...req.outputs.workspaceFiles.map(rebase)].map(
1339
1346
  (g) => normalizeGlob(g),
1340
1347
  )
@@ -1342,11 +1349,11 @@ export function outputPathSets(
1342
1349
  const files = new Set<string>()
1343
1350
  const dirs = new Set<string>()
1344
1351
  for (const glob of globs) {
1345
- const literal = globToOutputPath(glob)
1352
+ const { path: literal, rest } = literalHead(glob)
1346
1353
  paths.add(literal)
1347
1354
  // The legacy split has to GUESS what a path is; a wildcard-free glob was
1348
1355
  // declared as a file, a prefix cut at a wildcard is necessarily a dir.
1349
- if (literal === glob) files.add(literal)
1356
+ if (rest.length === 0) files.add(literal)
1350
1357
  else dirs.add(literal)
1351
1358
  }
1352
1359
  return {