@tempo-ai/visual-review 0.0.2 → 0.0.108-staging.1

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.
@@ -15,6 +15,63 @@ import path16 from "path";
15
15
 
16
16
  // ../canvas-capture-headless/src/capture.ts
17
17
  import { createHash } from "crypto";
18
+
19
+ // ../../modules/iframe-projection/combined/projection-pending.ts
20
+ var PROJECTION_PENDING_ATTRIBUTE = "data-tempo-projection-pending";
21
+
22
+ // ../../modules/component-render-pool/direct-render.ts
23
+ var DIRECT_RENDER_PARAMS = {
24
+ canvasPath: "canvasPath",
25
+ storyboardId: "storyboardId",
26
+ width: "width",
27
+ height: "height",
28
+ intrinsicSize: "intrinsicSize",
29
+ idle: "tempoCaptureIdle"
30
+ };
31
+ var STORYBOARD_RENDER_ID_ATTRIBUTE = "data-tempo-storyboard-render-id";
32
+ var STORYBOARD_PENDING_SELECTOR = `[${PROJECTION_PENDING_ATTRIBUTE}]`;
33
+ function buildCanvasStoryboardRenderId(canvasPath, storyboardId) {
34
+ return `canvas:${encodeURIComponent(canvasPath.trim())}#storyboard:${encodeURIComponent(
35
+ storyboardId.trim()
36
+ )}`;
37
+ }
38
+ function buildStoryboardRenderSelector(renderId) {
39
+ return `[${STORYBOARD_RENDER_ID_ATTRIBUTE}=${JSON.stringify(renderId)}]`;
40
+ }
41
+ function buildDirectRenderUrl(hostUrl, target) {
42
+ const hasWidth = target.width !== void 0;
43
+ const hasHeight = target.height !== void 0;
44
+ if (hasWidth !== hasHeight) {
45
+ throw new Error("Direct render width and height must be provided together");
46
+ }
47
+ const url = new URL(hostUrl);
48
+ url.searchParams.delete(DIRECT_RENDER_PARAMS.idle);
49
+ url.searchParams.set(DIRECT_RENDER_PARAMS.canvasPath, target.canvasPath);
50
+ url.searchParams.set(DIRECT_RENDER_PARAMS.storyboardId, target.storyboardId);
51
+ if (hasWidth && hasHeight) {
52
+ url.searchParams.set(DIRECT_RENDER_PARAMS.width, String(target.width));
53
+ url.searchParams.set(DIRECT_RENDER_PARAMS.height, String(target.height));
54
+ } else {
55
+ url.searchParams.delete(DIRECT_RENDER_PARAMS.width);
56
+ url.searchParams.delete(DIRECT_RENDER_PARAMS.height);
57
+ }
58
+ if (target.intrinsicSize) {
59
+ url.searchParams.set(DIRECT_RENDER_PARAMS.intrinsicSize, "1");
60
+ } else {
61
+ url.searchParams.delete(DIRECT_RENDER_PARAMS.intrinsicSize);
62
+ }
63
+ return url.toString();
64
+ }
65
+ function buildDirectRenderIdleUrl(hostUrl) {
66
+ const url = new URL(hostUrl);
67
+ for (const param of Object.values(DIRECT_RENDER_PARAMS)) {
68
+ url.searchParams.delete(param);
69
+ }
70
+ url.searchParams.set(DIRECT_RENDER_PARAMS.idle, "1");
71
+ return url.toString();
72
+ }
73
+
74
+ // ../canvas-capture-headless/src/capture.ts
18
75
  import { PNG } from "pngjs";
19
76
 
20
77
  // ../../modules/frame-pool/capture-determinism.ts
@@ -70,100 +127,240 @@ var FREEZE_MOTION_CSS = `
70
127
  }
71
128
  `;
72
129
 
73
- // ../canvas-capture-headless/src/determinism.ts
74
- var CHROMIUM_LAUNCH_ARGS = [
75
- "--force-color-profile=srgb",
76
- "--force-device-scale-factor=2",
77
- "--hide-scrollbars",
78
- "--disable-lcd-text"
130
+ // ../../modules/frame-pool/settle-script.ts
131
+ var SETTLE_STEPS = [
132
+ "target",
133
+ "fonts",
134
+ "suspense",
135
+ "content-quiet",
136
+ "paint"
79
137
  ];
80
- async function runSettleStep(name, work, onStep) {
81
- const startedAt = Date.now();
82
- try {
83
- return await work();
84
- } catch (error) {
85
- const message = error instanceof Error ? error.message : String(error);
86
- throw new Error(`settle.${name}: ${message}`);
87
- } finally {
88
- onStep?.(name, Date.now() - startedAt);
89
- }
90
- }
91
- async function settlePage(page, targetSelector, options = {}) {
92
- const quietMs = options.quietMs ?? 300;
93
- const timeoutMs = options.timeoutMs ?? 15e3;
94
- await runSettleStep(
95
- "target",
96
- () => page.waitForSelector(targetSelector, {
97
- state: "attached",
98
- timeout: timeoutMs
99
- }),
100
- options.onStep
101
- );
102
- await runSettleStep(
103
- "fonts",
104
- () => page.waitForFunction(() => document.fonts.status === "loaded", {
105
- timeout: timeoutMs
106
- }),
107
- options.onStep
138
+ var DEFAULT_SETTLE_QUIET_MS = 300;
139
+ var DEFAULT_SETTLE_STEP_TIMEOUT_MS = 15e3;
140
+ var DEFAULT_SETTLE_POLL_MS = 50;
141
+ function buildSettleWaitScript(options) {
142
+ const quietMs = Math.max(0, Math.round(options.quietMs ?? DEFAULT_SETTLE_QUIET_MS));
143
+ const stepTimeoutMs = Math.max(
144
+ 1,
145
+ Math.round(options.stepTimeoutMs ?? DEFAULT_SETTLE_STEP_TIMEOUT_MS)
108
146
  );
109
- await runSettleStep(
110
- "suspense",
111
- () => page.waitForFunction(
112
- (selector) => {
113
- const target = document.querySelector(selector);
114
- if (!target) return false;
115
- return !target.querySelector("[data-tempo-projection-pending]");
116
- },
117
- targetSelector,
118
- { timeout: timeoutMs }
119
- ),
120
- options.onStep
147
+ const quietTimeoutMs = Math.max(
148
+ 1,
149
+ Math.round(options.quietTimeoutMs ?? stepTimeoutMs)
121
150
  );
122
- await runSettleStep(
123
- "content-quiet",
124
- () => page.waitForFunction(
125
- ({ selector, quiet }) => {
126
- const target = document.querySelector(selector);
151
+ const pollMs = Math.max(1, Math.round(options.pollMs ?? DEFAULT_SETTLE_POLL_MS));
152
+ return `
153
+ (function() {
154
+ var selector = ${JSON.stringify(options.selector)};
155
+ var pendingSelector = ${JSON.stringify(options.pendingSelector ?? null)};
156
+ var quietMs = ${quietMs};
157
+ var stepTimeoutMs = ${stepTimeoutMs};
158
+ var quietTimeoutMs = ${quietTimeoutMs};
159
+ var quietBestEffort = ${options.quietBestEffort === true ? "true" : "false"};
160
+ var pollMs = ${pollMs};
161
+ var runPaint = ${options.paint === false ? "false" : "true"};
162
+ var steps = {};
163
+ // Cleared when the content-quiet step gives up under best effort.
164
+ var quiet = true;
165
+ // Monotonic and immune to the determinism kit's frozen Date.
166
+ var now = typeof performance !== 'undefined' && performance && typeof performance.now === 'function'
167
+ ? function() { return performance.now(); }
168
+ : function() { return Date.now(); };
169
+
170
+ function failure(step, message) {
171
+ return { ok: false, step: step, message: message, steps: steps };
172
+ }
173
+
174
+ // Polls \`check\` until it answers true, or \`budgetMs\` elapses. The
175
+ // first check runs synchronously so an already-settled step costs no
176
+ // poll interval. A step run with \`giveUp\` resolves at its budget
177
+ // instead of failing, recording that the content never held still.
178
+ function until(step, check, budgetMs, giveUp) {
179
+ var started = now();
180
+ return new Promise(function(resolve, reject) {
181
+ function tick() {
182
+ var done;
183
+ try {
184
+ done = check();
185
+ } catch (error) {
186
+ steps[step] = Math.round(now() - started);
187
+ reject(failure(step, String((error && error.message) || error)));
188
+ return;
189
+ }
190
+ if (done) {
191
+ steps[step] = Math.round(now() - started);
192
+ resolve();
193
+ return;
194
+ }
195
+ if (now() - started >= budgetMs) {
196
+ steps[step] = Math.round(now() - started);
197
+ if (giveUp) {
198
+ quiet = false;
199
+ resolve();
200
+ return;
201
+ }
202
+ reject(failure(step, 'Timeout ' + budgetMs + 'ms exceeded'));
203
+ return;
204
+ }
205
+ setTimeout(tick, pollMs);
206
+ }
207
+ tick();
208
+ });
209
+ }
210
+
211
+ var quietKey = null;
212
+ var quietSince = 0;
213
+ // Images that take part in the capture: laid out and not hidden. A
214
+ // hidden responsive variant never starts native lazy loading and must
215
+ // not hold the window; a rendered lazy image never starts either,
216
+ // because a capture does not scroll, so it is asked for eagerly.
217
+ function renderedImages(target) {
218
+ var all = Array.prototype.slice.call(target.querySelectorAll('img'));
219
+ var rendered = [];
220
+ for (var i = 0; i < all.length; i++) {
221
+ var img = all[i];
222
+ if (img.getClientRects().length === 0) continue;
223
+ var visibility = getComputedStyle(img).visibility;
224
+ if (visibility === 'hidden' || visibility === 'collapse') continue;
225
+ rendered.push(img);
226
+ }
227
+ return rendered;
228
+ }
229
+ function contentQuiet() {
230
+ var target = document.querySelector(selector);
127
231
  if (!target) return false;
128
- const images = Array.from(target.querySelectorAll("img"));
129
- if (images.some((img) => !img.complete)) return false;
130
- const rect = target.getBoundingClientRect();
131
- const text = target.innerText ?? "";
132
- const key2 = [
232
+ var images = renderedImages(target);
233
+ for (var i = 0; i < images.length; i++) {
234
+ if (images[i].loading === 'lazy') images[i].loading = 'eager';
235
+ }
236
+ for (var j = 0; j < images.length; j++) {
237
+ if (!images[j].complete) {
238
+ // Load/error handlers can replace content without moving its
239
+ // geometry: start a fresh quiet window once they have run.
240
+ quietKey = null;
241
+ return false;
242
+ }
243
+ }
244
+ var rect = target.getBoundingClientRect();
245
+ var text = target.innerText || '';
246
+ var key = [
133
247
  rect.x,
134
248
  rect.y,
135
249
  rect.width,
136
250
  rect.height,
137
- target.querySelectorAll("*").length,
251
+ target.querySelectorAll('*').length,
138
252
  text.length,
139
253
  images.length
140
- ].join(",");
141
- const state = window.__tempoVisualReviewQuiet ?? {};
142
- window.__tempoVisualReviewQuiet = state;
143
- const now = performance.now();
144
- if (state.key !== key2) {
145
- state.key = key2;
146
- state.since = now;
254
+ ].join(',');
255
+ var at = now();
256
+ if (quietKey !== key) {
257
+ quietKey = key;
258
+ quietSince = at;
147
259
  return false;
148
260
  }
149
- return now - state.since >= quiet;
150
- },
151
- { selector: targetSelector, quiet: quietMs },
152
- { timeout: timeoutMs, polling: 50 }
153
- ),
154
- options.onStep
155
- );
156
- await runSettleStep(
157
- "paint",
158
- () => page.evaluate(
159
- () => new Promise(
160
- (resolve2) => requestAnimationFrame(() => requestAnimationFrame(() => resolve2()))
161
- )
162
- ),
163
- options.onStep
261
+ return at - quietSince >= quietMs;
262
+ }
263
+
264
+ function paint() {
265
+ if (!runPaint) return Promise.resolve();
266
+ var started = now();
267
+ return new Promise(function(resolve) {
268
+ requestAnimationFrame(function() {
269
+ requestAnimationFrame(function() {
270
+ steps.paint = Math.round(now() - started);
271
+ resolve();
272
+ });
273
+ });
274
+ });
275
+ }
276
+
277
+ return until('target', function() {
278
+ return document.querySelector(selector) !== null;
279
+ }, stepTimeoutMs, false)
280
+ .then(function() {
281
+ return until('fonts', function() {
282
+ var fonts = document.fonts;
283
+ return !fonts || fonts.status === 'loaded';
284
+ }, stepTimeoutMs, false);
285
+ })
286
+ .then(function() {
287
+ return until('suspense', function() {
288
+ if (!pendingSelector) return true;
289
+ var target = document.querySelector(selector);
290
+ if (!target) return false;
291
+ return !target.querySelector(pendingSelector);
292
+ }, stepTimeoutMs, false);
293
+ })
294
+ .then(function() {
295
+ return until('content-quiet', contentQuiet, quietTimeoutMs, quietBestEffort);
296
+ })
297
+ .then(paint)
298
+ .then(
299
+ function() {
300
+ return { ok: true, steps: steps, quiet: quiet };
301
+ },
302
+ function(caught) {
303
+ if (caught && caught.ok === false) return caught;
304
+ return failure('paint', String((caught && caught.message) || caught));
305
+ }
306
+ );
307
+ })()
308
+ `;
309
+ }
310
+
311
+ // ../canvas-capture-headless/src/navigation-errors.ts
312
+ function isNavigationInterruptedError(message) {
313
+ return /Execution context was destroyed|most likely because of a navigation|Cannot find context with specified id|[Ff]rame was detached|Navigation interrupted|is interrupted by another navigation|Inspected target navigated or closed|Cannot find object with id|context was destroyed/.test(
314
+ message
164
315
  );
165
316
  }
166
317
 
318
+ // ../canvas-capture-headless/src/determinism.ts
319
+ var CHROMIUM_LAUNCH_ARGS = [
320
+ "--force-color-profile=srgb",
321
+ "--force-device-scale-factor=2",
322
+ "--hide-scrollbars",
323
+ "--disable-lcd-text"
324
+ ];
325
+ async function settlePage(page, targetSelector, options = {}) {
326
+ const quietMs = options.quietMs ?? 300;
327
+ const timeoutMs = options.timeoutMs ?? 15e3;
328
+ const budgetMs = timeoutMs * SETTLE_STEPS.length;
329
+ const deadline = Date.now() + budgetMs;
330
+ let resumes = 0;
331
+ for (; ; ) {
332
+ let report;
333
+ try {
334
+ report = await page.evaluate(
335
+ buildSettleWaitScript({
336
+ selector: targetSelector,
337
+ pendingSelector: STORYBOARD_PENDING_SELECTOR,
338
+ quietMs,
339
+ stepTimeoutMs: Math.max(1, Math.min(timeoutMs, deadline - Date.now()))
340
+ })
341
+ );
342
+ } catch (error) {
343
+ const message = error instanceof Error ? error.message : String(error);
344
+ if (!isNavigationInterruptedError(message)) throw error;
345
+ resumes += 1;
346
+ if (Date.now() >= deadline) {
347
+ throw new Error(
348
+ `settle: Timeout ${budgetMs}ms exceeded; the document was replaced ${resumes} times while waiting`
349
+ );
350
+ }
351
+ continue;
352
+ }
353
+ for (const step of SETTLE_STEPS) {
354
+ const ms = report.steps[step];
355
+ if (ms !== void 0) options.onStep?.(step, ms);
356
+ }
357
+ if (!report.ok) {
358
+ throw new Error(`settle.${report.step}: ${report.message}`);
359
+ }
360
+ return;
361
+ }
362
+ }
363
+
167
364
  // ../canvas-capture-headless/src/ensure-chromium.ts
168
365
  import { spawn } from "child_process";
169
366
  import { createRequire } from "module";
@@ -223,7 +420,7 @@ async function installChromium(log, spawnImpl = spawn) {
223
420
  }
224
421
 
225
422
  // ../canvas-capture-headless/src/fingerprint.ts
226
- var KIT_VERSION = "2";
423
+ var KIT_VERSION = "3";
227
424
  var FONTS_BUNDLE_VERSION = "system-1";
228
425
  var DEVICE_SCALE_FACTOR = 2;
229
426
  function buildEnvFingerprint(input) {
@@ -534,6 +731,9 @@ function installRenderProfileInPage() {
534
731
  const renderers = /* @__PURE__ */ new Map();
535
732
  profileWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
536
733
  supportsFiber: true,
734
+ // Consumers that also load `modules/iframe-projection` see this window
735
+ // property through its global `DevToolsHook` augmentation, where
736
+ // `renderers` is typed by ReactRenderer; this stub only counts them.
537
737
  renderers,
538
738
  inject(renderer) {
539
739
  profile.reactInjectedMs ??= performance.now() - profile.startedAtMs;
@@ -607,15 +807,15 @@ function readRenderProfileInPage(targetCanvasPath) {
607
807
 
608
808
  // ../canvas-capture-headless/src/capture.ts
609
809
  function storyboardRenderId(hostCanvasPath, storyboardId) {
610
- return `canvas:${encodeURIComponent(hostCanvasPath.trim())}#storyboard:${encodeURIComponent(storyboardId.trim())}`;
611
- }
612
- function buildDirectRenderUrl(hostUrl, target) {
613
- const url = new URL(hostUrl);
614
- url.searchParams.set("canvasPath", target.hostCanvasPath);
615
- url.searchParams.set("storyboardId", target.storyboardId);
616
- url.searchParams.set("width", String(target.width));
617
- url.searchParams.set("height", String(target.height));
618
- return url.toString();
810
+ return buildCanvasStoryboardRenderId(hostCanvasPath, storyboardId);
811
+ }
812
+ function buildDirectRenderUrl2(hostUrl, target) {
813
+ return buildDirectRenderUrl(hostUrl, {
814
+ canvasPath: target.hostCanvasPath,
815
+ storyboardId: target.storyboardId,
816
+ width: target.width,
817
+ height: target.height
818
+ });
619
819
  }
620
820
  function sha256Hex(bytes) {
621
821
  return createHash("sha256").update(bytes).digest("hex");
@@ -636,11 +836,6 @@ function looksBlank(pngBytes) {
636
836
  }
637
837
  return true;
638
838
  }
639
- function isNavigationInterruptedError(message) {
640
- return /Execution context was destroyed|most likely because of a navigation|Cannot find context with specified id|[Ff]rame was detached|Navigation interrupted|is interrupted by another navigation|Inspected target navigated or closed|Cannot find object with id|context was destroyed/.test(
641
- message
642
- );
643
- }
644
839
  function isPageGoneError(message) {
645
840
  return /Target closed|Target page, context or browser has been closed|Page closed|Target crashed/.test(
646
841
  message
@@ -835,7 +1030,7 @@ async function startCaptureSession(options) {
835
1030
  target.hostCanvasPath,
836
1031
  target.storyboardId
837
1032
  );
838
- const selector = `[data-tempo-storyboard-render-id=${JSON.stringify(renderId)}]`;
1033
+ const selector = buildStoryboardRenderSelector(renderId);
839
1034
  const ownsWorker = reusableWorker === void 0;
840
1035
  const worker = reusableWorker ?? await createCaptureWorker(capture);
841
1036
  const { page } = worker;
@@ -849,19 +1044,17 @@ async function startCaptureSession(options) {
849
1044
  try {
850
1045
  const navStart = Date.now();
851
1046
  try {
852
- const targetUrl = buildDirectRenderUrl(options.hostUrl, target);
1047
+ const targetUrl = buildDirectRenderUrl2(options.hostUrl, target);
853
1048
  if (!worker.hasNavigated) {
854
1049
  await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
855
1050
  await page.addStyleTag({ content: FREEZE_MOTION_CSS });
856
1051
  worker.hasNavigated = true;
857
1052
  } else {
858
1053
  if (worker.currentSelector === selector) {
859
- const idleUrl = new URL(options.hostUrl);
860
- idleUrl.searchParams.set("tempoCaptureIdle", "1");
861
1054
  await page.evaluate((url) => {
862
1055
  history.replaceState(null, "", url);
863
1056
  window.dispatchEvent(new PopStateEvent("popstate"));
864
- }, idleUrl.toString());
1057
+ }, buildDirectRenderIdleUrl(options.hostUrl));
865
1058
  worker.counters.sameDocumentIssued += 1;
866
1059
  await page.waitForSelector(worker.currentSelector, {
867
1060
  state: "detached",
@@ -872,7 +1065,6 @@ async function startCaptureSession(options) {
872
1065
  ({ url, nextSelector }) => {
873
1066
  const captureWindow = window;
874
1067
  captureWindow.__tempoResetCaptureDeterminism?.();
875
- delete captureWindow.__tempoVisualReviewQuiet;
876
1068
  captureWindow.__tempoCaptureRenderProfile?.begin(nextSelector);
877
1069
  history.replaceState(null, "", url);
878
1070
  window.dispatchEvent(new PopStateEvent("popstate"));
@@ -1437,6 +1629,7 @@ function abortableWait(cancelledMessage) {
1437
1629
  import { spawn as spawn3 } from "child_process";
1438
1630
  import path2 from "path";
1439
1631
  var BUNDLED_NODE_BIN_DIR_ENV = "TEMPO_NODE_TOOLCHAIN_BIN_DIR";
1632
+ var BUNDLED_NODE_RUNTIME_ENV = "TEMPO_NODE_TOOLCHAIN_RUNTIME";
1440
1633
  function getBundledNodeBinDir() {
1441
1634
  const dir = process.env[BUNDLED_NODE_BIN_DIR_ENV];
1442
1635
  return dir && dir.trim().length > 0 ? dir : null;
@@ -1455,10 +1648,13 @@ function shouldUseBundledNode(args) {
1455
1648
  }
1456
1649
  function applyBundledNodeEnv(env, binDir) {
1457
1650
  if (!binDir) return env;
1458
- return {
1459
- ...prependDirToPathEnv(env, binDir),
1460
- ELECTRON_RUN_AS_NODE: "1"
1461
- };
1651
+ const next = prependDirToPathEnv(env, binDir);
1652
+ if (env[BUNDLED_NODE_RUNTIME_ENV] === "native") {
1653
+ delete next.ELECTRON_RUN_AS_NODE;
1654
+ } else {
1655
+ next.ELECTRON_RUN_AS_NODE = "1";
1656
+ }
1657
+ return next;
1462
1658
  }
1463
1659
  function prependDirToPathEnv(env, dir) {
1464
1660
  const pathKey = Object.keys(env).find((key2) => key2.toLowerCase() === "path");
@@ -4603,7 +4799,7 @@ async function parcelEventToFileEvent(event, rootInfo) {
4603
4799
  // ../tempo-sdk/package.json
4604
4800
  var package_default = {
4605
4801
  name: "tempo-sdk",
4606
- version: "0.0.38",
4802
+ version: "0.0.42",
4607
4803
  type: "module",
4608
4804
  description: "Tempo SDK \u2014 Vite, Next.js, and Expo plugins for JSX annotation and shared page/storyboard types",
4609
4805
  repository: {
@@ -12233,6 +12429,44 @@ async function readTempoDevserverConfig(tempoConfigPath) {
12233
12429
  tempoHostUrl: config.urls?.tempoHost ?? null
12234
12430
  };
12235
12431
  }
12432
+ function projectRootForTempoDir(tempoDir) {
12433
+ const resolved = path14.resolve(tempoDir);
12434
+ return path14.basename(resolved) === "tempo" ? path14.dirname(resolved) : resolved;
12435
+ }
12436
+ async function resolveCanvasHostLayout(options) {
12437
+ const repoRoot = path14.resolve(options.repoRoot);
12438
+ const candidates = options.tempoDir ? [path14.join(path14.resolve(options.tempoDir), "tempo.config.json")] : [
12439
+ path14.join(repoRoot, "tempo", "tempo.config.json"),
12440
+ path14.join(repoRoot, "tempo.config.json")
12441
+ ];
12442
+ for (const configPath of candidates) {
12443
+ let parsed;
12444
+ try {
12445
+ parsed = JSON.parse(await readFile10(configPath, "utf-8"));
12446
+ } catch {
12447
+ continue;
12448
+ }
12449
+ if (typeof parsed !== "object" || parsed === null || !("scripts" in parsed)) {
12450
+ continue;
12451
+ }
12452
+ const tempoDir2 = path14.dirname(configPath);
12453
+ const paths = parsed.paths;
12454
+ const canvases = typeof paths?.canvases === "string" && paths.canvases.trim().length > 0 ? paths.canvases.trim().split("\\").join("/") : "designs";
12455
+ return {
12456
+ tempoDir: tempoDir2,
12457
+ projectRoot: projectRootForTempoDir(tempoDir2),
12458
+ pagesDir: path14.resolve(tempoDir2, canvases),
12459
+ configPath
12460
+ };
12461
+ }
12462
+ const tempoDir = options.tempoDir ? path14.resolve(options.tempoDir) : path14.join(repoRoot, "tempo");
12463
+ return {
12464
+ tempoDir,
12465
+ projectRoot: projectRootForTempoDir(tempoDir),
12466
+ pagesDir: path14.join(tempoDir, "designs"),
12467
+ configPath: null
12468
+ };
12469
+ }
12236
12470
  function resolveTempoDir(options) {
12237
12471
  if (options.tempoConfigPath) return path14.dirname(options.tempoConfigPath);
12238
12472
  if (options.tempoDir) return options.tempoDir;
@@ -12317,7 +12551,7 @@ async function startDevserverSession(options) {
12317
12551
  scripts: config.scripts
12318
12552
  });
12319
12553
  await devServer.initialize();
12320
- const projectRoot = path14.dirname(tempoDir);
12554
+ const projectRoot = projectRootForTempoDir(tempoDir);
12321
12555
  for (const hostCanvasPath of hostCanvasPaths) {
12322
12556
  await devServer.ensureCanvasRegistered(
12323
12557
  path14.join(projectRoot, hostCanvasPath)
@@ -12393,14 +12627,14 @@ async function startDevserverSession(options) {
12393
12627
  }
12394
12628
 
12395
12629
  // ../canvas-capture-headless/src/enumerate.ts
12396
- import { readFile as readFile11 } from "fs/promises";
12630
+ import { access as access4, readFile as readFile11 } from "fs/promises";
12397
12631
  import path15 from "path";
12398
12632
  function toPosixPath2(value) {
12399
12633
  return value.split(path15.sep).join("/");
12400
12634
  }
12401
12635
  async function enumerateCanvasStoryboards(options) {
12402
12636
  const pagesDir = options.pagesDir ?? path15.join(options.tempoDir, "designs");
12403
- const projectRoot = path15.dirname(options.tempoDir);
12637
+ const projectRoot = projectRootForTempoDir(options.tempoDir);
12404
12638
  const canvasFiles = await scanCanvasFiles(pagesDir);
12405
12639
  const storyboards = [];
12406
12640
  const structuralErrors = [];
@@ -12465,6 +12699,61 @@ async function enumerateCanvasStoryboards(options) {
12465
12699
  vrtConfigErrors
12466
12700
  };
12467
12701
  }
12702
+ async function enumerateTrackedStoryboards(options) {
12703
+ try {
12704
+ await access4(options.tempoDir);
12705
+ } catch {
12706
+ return {
12707
+ tracked: [],
12708
+ paused: [],
12709
+ totalStoryboards: 0,
12710
+ totalCanvases: 0,
12711
+ errors: []
12712
+ };
12713
+ }
12714
+ const enumeration = await enumerateCanvasStoryboards(options);
12715
+ const tracked = [];
12716
+ const paused = [];
12717
+ const errors = [
12718
+ ...enumeration.structuralErrors,
12719
+ ...enumeration.vrtConfigErrors
12720
+ ];
12721
+ for (const storyboard of enumeration.storyboards) {
12722
+ if (!storyboard.vrt) continue;
12723
+ const board = {
12724
+ canvasPath: storyboard.canvasPath,
12725
+ hostCanvasPath: storyboard.hostCanvasPath,
12726
+ storyboardId: storyboard.storyboardId,
12727
+ name: storyboard.name,
12728
+ layout: storyboard.layout,
12729
+ threshold: storyboard.vrt.threshold,
12730
+ ignore: storyboard.vrt.ignore
12731
+ };
12732
+ const maskedArea = (storyboard.vrt.ignore ?? []).reduce(
12733
+ (total, region) => total + Math.max(0, Math.min(region.w, board.layout.width)) * Math.max(0, Math.min(region.h, board.layout.height)),
12734
+ 0
12735
+ );
12736
+ if (maskedArea >= 0.9 * board.layout.width * board.layout.height) {
12737
+ errors.push({
12738
+ filePath: storyboard.filePath,
12739
+ message: `<Storyboard id="${storyboard.storyboardId}"> vrt.ignore masks \u226590% of the board \u2014 untrack it instead`
12740
+ });
12741
+ continue;
12742
+ }
12743
+ if (storyboard.vrt.skip) {
12744
+ paused.push(board);
12745
+ continue;
12746
+ }
12747
+ tracked.push(board);
12748
+ }
12749
+ return {
12750
+ tracked,
12751
+ paused,
12752
+ totalStoryboards: enumeration.totalStoryboards,
12753
+ totalCanvases: enumeration.totalCanvases,
12754
+ errors
12755
+ };
12756
+ }
12468
12757
 
12469
12758
  // ../canvas-capture-headless/src/timing.ts
12470
12759
  function percentile(sortedValues, p) {
@@ -12544,64 +12833,8 @@ var TimingReport = class {
12544
12833
  };
12545
12834
 
12546
12835
  // src/enumerate.ts
12547
- import { access as access4 } from "fs/promises";
12548
- async function enumerateTrackedStoryboards(options) {
12549
- try {
12550
- await access4(options.tempoDir);
12551
- } catch {
12552
- return {
12553
- tracked: [],
12554
- paused: [],
12555
- totalStoryboards: 0,
12556
- totalCanvases: 0,
12557
- errors: []
12558
- };
12559
- }
12560
- const enumeration = await enumerateCanvasStoryboards({
12561
- tempoDir: options.tempoDir,
12562
- pagesDir: options.pagesDir
12563
- });
12564
- const tracked = [];
12565
- const paused = [];
12566
- const errors = [
12567
- ...enumeration.structuralErrors,
12568
- ...enumeration.vrtConfigErrors
12569
- ];
12570
- for (const storyboard of enumeration.storyboards) {
12571
- if (!storyboard.vrt) continue;
12572
- const board = {
12573
- canvasPath: storyboard.canvasPath,
12574
- hostCanvasPath: storyboard.hostCanvasPath,
12575
- storyboardId: storyboard.storyboardId,
12576
- name: storyboard.name,
12577
- layout: storyboard.layout,
12578
- threshold: storyboard.vrt.threshold,
12579
- ignore: storyboard.vrt.ignore
12580
- };
12581
- const maskedArea = (storyboard.vrt.ignore ?? []).reduce(
12582
- (total, region) => total + Math.max(0, Math.min(region.w, board.layout.width)) * Math.max(0, Math.min(region.h, board.layout.height)),
12583
- 0
12584
- );
12585
- if (maskedArea >= 0.9 * board.layout.width * board.layout.height) {
12586
- errors.push({
12587
- filePath: storyboard.filePath,
12588
- message: `<Storyboard id="${storyboard.storyboardId}"> vrt.ignore masks \u226590% of the board \u2014 untrack it instead`
12589
- });
12590
- continue;
12591
- }
12592
- if (storyboard.vrt.skip) {
12593
- paused.push(board);
12594
- continue;
12595
- }
12596
- tracked.push(board);
12597
- }
12598
- return {
12599
- tracked,
12600
- paused,
12601
- totalStoryboards: enumeration.totalStoryboards,
12602
- totalCanvases: enumeration.totalCanvases,
12603
- errors
12604
- };
12836
+ async function enumerateTrackedStoryboards2(options) {
12837
+ return await enumerateTrackedStoryboards(options);
12605
12838
  }
12606
12839
 
12607
12840
  // src/fingerprint.ts
@@ -12824,6 +13057,7 @@ function parseArgs(argv) {
12824
13057
  const args = {
12825
13058
  repoRoot: inferRepoRoot(),
12826
13059
  tempoDir: "",
13060
+ tempoDirExplicit: false,
12827
13061
  // The published CLI is intentionally near-argumentless in Actions. Local
12828
13062
  // capture remains Convex-free unless --upload is explicit.
12829
13063
  upload: process.env.GITHUB_ACTIONS === "true" && inferredTrigger !== "manual",
@@ -12854,6 +13088,7 @@ function parseArgs(argv) {
12854
13088
  break;
12855
13089
  case "--tempo-dir":
12856
13090
  args.tempoDir = path16.resolve(next());
13091
+ args.tempoDirExplicit = true;
12857
13092
  break;
12858
13093
  case "--out":
12859
13094
  args.out = path16.resolve(next());
@@ -13046,6 +13281,12 @@ function startClaimHeartbeat(options) {
13046
13281
  }
13047
13282
  };
13048
13283
  }
13284
+ function explicitTempoDirRelative(args) {
13285
+ if (!args.tempoDirExplicit) return void 0;
13286
+ const relative2 = path16.relative(args.repoRoot, args.tempoDir);
13287
+ if (relative2.startsWith("..") || path16.isAbsolute(relative2)) return void 0;
13288
+ return relative2;
13289
+ }
13049
13290
  function buildBaselineCaptureArgs(input) {
13050
13291
  const args = [
13051
13292
  "capture",
@@ -13067,6 +13308,12 @@ function buildBaselineCaptureArgs(input) {
13067
13308
  ];
13068
13309
  if (input.url) args.push("--url", input.url);
13069
13310
  if (input.githubRepo) args.push("--github-repo", input.githubRepo);
13311
+ if (input.tempoDirRelative !== void 0) {
13312
+ args.push(
13313
+ "--tempo-dir",
13314
+ path16.join(input.worktreeRoot, input.tempoDirRelative || ".")
13315
+ );
13316
+ }
13070
13317
  if (input.repoKey) args.push("--repo-key", input.repoKey);
13071
13318
  if (input.settleTimeoutMs !== void 0) {
13072
13319
  args.push("--settle-timeout", String(input.settleTimeoutMs));
@@ -13101,7 +13348,8 @@ async function runBaselineCaptureInWorktree(options) {
13101
13348
  url: options.args.url,
13102
13349
  githubRepo: options.args.githubRepo,
13103
13350
  repoKey: options.args.repoKey,
13104
- settleTimeoutMs: options.args.settleTimeoutMs
13351
+ settleTimeoutMs: options.args.settleTimeoutMs,
13352
+ tempoDirRelative: explicitTempoDirRelative(options.args)
13105
13353
  })
13106
13354
  ];
13107
13355
  await new Promise((resolve2, reject) => {
@@ -13152,10 +13400,15 @@ async function main(argv = process.argv.slice(2)) {
13152
13400
  if (args.trigger === "pr" && baseCandidates.length === 0) {
13153
13401
  throw new Error("--trigger pr requires --base <merge-base sha>");
13154
13402
  }
13155
- const pagesDir = path16.join(args.tempoDir, "designs");
13403
+ const layout = await resolveCanvasHostLayout({
13404
+ repoRoot: args.repoRoot,
13405
+ tempoDir: args.tempoDirExplicit ? args.tempoDir : void 0
13406
+ });
13407
+ args.tempoDir = layout.tempoDir;
13408
+ const pagesDir = layout.pagesDir;
13156
13409
  const enumeration = await timing.phase(
13157
13410
  "enumerate",
13158
- () => enumerateTrackedStoryboards({ tempoDir: args.tempoDir, pagesDir })
13411
+ () => enumerateTrackedStoryboards2({ tempoDir: args.tempoDir, pagesDir })
13159
13412
  );
13160
13413
  if (enumeration.errors.length > 0) {
13161
13414
  for (const error of enumeration.errors) {
@@ -13608,13 +13861,14 @@ export {
13608
13861
  percentile,
13609
13862
  summarize,
13610
13863
  TimingReport,
13611
- enumerateTrackedStoryboards,
13864
+ enumerateTrackedStoryboards2 as enumerateTrackedStoryboards,
13612
13865
  scaleIgnoreRegionsToDevicePx,
13613
13866
  VisualReviewHttpError,
13614
13867
  VisualReviewIngestClient,
13615
13868
  resolveOrClaimBaseline,
13869
+ explicitTempoDirRelative,
13616
13870
  buildBaselineCaptureArgs,
13617
13871
  main,
13618
13872
  VISUAL_REVIEW_OIDC_AUDIENCE
13619
13873
  };
13620
- //# sourceMappingURL=chunk-HTDJZ32L.js.map
13874
+ //# sourceMappingURL=chunk-WGNWCWMJ.js.map