@reshotdev/screenshot 0.0.1-beta.12 → 0.0.1-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +67 -22
  2. package/package.json +18 -14
  3. package/src/commands/auth.js +37 -7
  4. package/src/commands/capture-dom.js +50 -0
  5. package/src/commands/compose.js +220 -0
  6. package/src/commands/doctor-release.js +7 -0
  7. package/src/commands/doctor-target.js +36 -4
  8. package/src/commands/drifts.js +13 -1
  9. package/src/commands/publish.js +183 -21
  10. package/src/commands/pull.js +9 -4
  11. package/src/commands/refresh.js +166 -0
  12. package/src/commands/setup-wizard.js +57 -3
  13. package/src/commands/status.js +22 -2
  14. package/src/commands/variation.js +194 -0
  15. package/src/index.js +190 -10
  16. package/src/lib/api-client.js +61 -35
  17. package/src/lib/auto-update/refresh.js +598 -0
  18. package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
  19. package/src/lib/auto-update/spec.js +89 -0
  20. package/src/lib/capture-engine.js +76 -2
  21. package/src/lib/capture-script-runner.js +289 -138
  22. package/src/lib/certification.js +23 -1
  23. package/src/lib/compose-context.js +156 -0
  24. package/src/lib/compose-pack.js +42 -0
  25. package/src/lib/compose-runtime.js +34 -0
  26. package/src/lib/compose-upload.js +142 -0
  27. package/src/lib/config.js +2 -2
  28. package/src/lib/dom-capture.js +64 -0
  29. package/src/lib/ensure-browser.js +147 -0
  30. package/src/lib/record-clip.js +83 -3
  31. package/src/lib/record-config.js +0 -4
  32. package/src/lib/release-doctor.js +11 -3
  33. package/src/lib/resolve-targets.js +60 -0
  34. package/src/lib/run-manifest.js +45 -0
  35. package/src/lib/ui-api-helpers.js +118 -0
  36. package/src/lib/ui-api.js +28 -820
  37. package/src/lib/ui-asset-cleanup.js +62 -0
  38. package/src/lib/ui-output-versions.js +165 -0
  39. package/src/lib/ui-recorder-routes.js +341 -0
  40. package/src/lib/ui-scenario-metadata.js +161 -0
  41. package/vendor/compose/dist/auto-update.cjs +5544 -0
  42. package/vendor/compose/dist/auto-update.mjs +5518 -0
  43. package/vendor/compose/dist/capture.cjs +1450 -0
  44. package/vendor/compose/dist/capture.mjs +1416 -0
  45. package/vendor/compose/dist/eligibility.cjs +5331 -0
  46. package/vendor/compose/dist/eligibility.mjs +5313 -0
  47. package/vendor/compose/dist/index.cjs +2046 -0
  48. package/vendor/compose/dist/index.mjs +1997 -0
  49. package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
  50. package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
  51. package/vendor/compose/dist/jsx-runtime.cjs +58 -0
  52. package/vendor/compose/dist/jsx-runtime.mjs +31 -0
  53. package/vendor/compose/dist/render.cjs +558 -0
  54. package/vendor/compose/dist/render.mjs +515 -0
  55. package/vendor/compose/dist/verify-cli.cjs +3806 -0
  56. package/vendor/compose/dist/verify-cli.mjs +3812 -0
  57. package/vendor/compose/dist/verify.cjs +3880 -0
  58. package/vendor/compose/dist/verify.mjs +3858 -0
  59. package/web/manager/dist/assets/{index-CvleJUur.js → index-D0S2otug.js} +56 -56
  60. package/web/manager/dist/index.html +1 -1
  61. package/src/commands/ingest.js +0 -458
  62. package/src/commands/setup.js +0 -165
  63. package/src/lib/playwright-runner.js +0 -252
@@ -0,0 +1,89 @@
1
+ // Phase 5 auto-update — local per-composition state ("the stored spec").
2
+ //
3
+ // A composition's source screen (URL + viewport), the accepted baseline's
4
+ // structure signature + reference frame, and any outstanding flagged candidate
5
+ // live on disk so a CI `reshot refresh` is reproducible and idempotent. The
6
+ // reference frame is the prior ACCEPTED render's recapture; it advances only when
7
+ // a refresh publishes, so a flagged redesign leaves the old baseline (and the old
8
+ // live clip) untouched.
9
+
10
+ const fs = require("fs-extra");
11
+ const path = require("path");
12
+
13
+ function baseDir() {
14
+ return (
15
+ process.env.RESHOT_AUTO_UPDATE_DIR ||
16
+ path.join(process.cwd(), ".reshot", "auto-update")
17
+ );
18
+ }
19
+
20
+ function specDir(compositionId) {
21
+ return path.join(baseDir(), compositionId);
22
+ }
23
+
24
+ function specPath(compositionId) {
25
+ return path.join(specDir(compositionId), "spec.json");
26
+ }
27
+
28
+ function referencePngPath(compositionId) {
29
+ return path.join(specDir(compositionId), "reference.png");
30
+ }
31
+
32
+ async function writeSpec(spec) {
33
+ if (!spec || !spec.compositionId) {
34
+ throw new Error("writeSpec requires spec.compositionId");
35
+ }
36
+ await fs.ensureDir(specDir(spec.compositionId));
37
+ await fs.writeJson(specPath(spec.compositionId), spec, { spaces: 2 });
38
+ return spec;
39
+ }
40
+
41
+ async function readSpec(compositionId) {
42
+ const file = specPath(compositionId);
43
+ if (!(await fs.pathExists(file))) {
44
+ throw new Error(
45
+ `No auto-update spec for composition ${compositionId} at ${file}. ` +
46
+ "Register it with `reshot refresh --register` (or seed it) first.",
47
+ );
48
+ }
49
+ return fs.readJson(file);
50
+ }
51
+
52
+ async function listSpecs(projectId) {
53
+ const dir = baseDir();
54
+ if (!(await fs.pathExists(dir))) return [];
55
+ const entries = await fs.readdir(dir);
56
+ const specs = [];
57
+ for (const entry of entries) {
58
+ const file = specPath(entry);
59
+ if (!(await fs.pathExists(file))) continue;
60
+ const spec = await fs.readJson(file);
61
+ if (projectId && spec.projectId !== projectId) continue;
62
+ specs.push(spec);
63
+ }
64
+ // Stable order so CI summaries and idempotence checks are deterministic.
65
+ return specs.sort((a, b) => String(a.slug).localeCompare(String(b.slug)));
66
+ }
67
+
68
+ async function readReferencePng(compositionId) {
69
+ const file = referencePngPath(compositionId);
70
+ if (!(await fs.pathExists(file))) return null;
71
+ return fs.readFile(file);
72
+ }
73
+
74
+ async function writeReferencePng(compositionId, buffer) {
75
+ await fs.ensureDir(specDir(compositionId));
76
+ await fs.writeFile(referencePngPath(compositionId), buffer);
77
+ }
78
+
79
+ module.exports = {
80
+ baseDir,
81
+ specDir,
82
+ specPath,
83
+ referencePngPath,
84
+ writeSpec,
85
+ readSpec,
86
+ listSpecs,
87
+ readReferencePng,
88
+ writeReferencePng,
89
+ };
@@ -6,6 +6,7 @@ const path = require("path");
6
6
  const fs = require("fs-extra");
7
7
  const chalk = require("chalk");
8
8
  const { buildLaunchOptions } = require("./ci-detect");
9
+ const { launchChromium } = require("./ensure-browser");
9
10
  const {
10
11
  applyVariantToPage,
11
12
  applyStorageAndReload,
@@ -145,6 +146,13 @@ class CaptureEngine {
145
146
  // Style configuration (image beautification post-capture)
146
147
  this.styleConfig = options.styleConfig || null;
147
148
 
149
+ // DOM scene (MHTML) capture toggle — emits a self-contained Chromium
150
+ // MHTML bundle alongside each PNG so variations can be rendered from
151
+ // the captured DOM without re-running the live app. Defaults to ON;
152
+ // opt out via reshot.config.json -> { domScene: false } or per-scenario
153
+ // -> { domScene: false }.
154
+ this.domSceneEnabled = options.domScene !== false;
155
+
148
156
  // Legacy support for old variant format
149
157
  if (!this.variantConfig && options.variant) {
150
158
  this.variantConfig = this._convertLegacyVariant(options.variant);
@@ -292,9 +300,9 @@ class CaptureEngine {
292
300
 
293
301
  const contextOptions = this._buildContextOptions();
294
302
 
295
- this.browser = await chromium.launch(buildLaunchOptions({
303
+ this.browser = await launchChromium(chromium, buildLaunchOptions({
296
304
  headless: this.headless,
297
- }));
305
+ }), this.logger);
298
306
  this.context = await this.browser.newContext(contextOptions);
299
307
  this.page = await this.context.newPage();
300
308
 
@@ -623,6 +631,32 @@ class CaptureEngine {
623
631
  // Wait for network to settle
624
632
  await this._waitForStability();
625
633
 
634
+ // Post-stability auth redirect check: catches SPA redirects that happen
635
+ // after client-side JS has executed (the pre-stability check at domcontentloaded
636
+ // may miss these since JS hasn't finished routing yet)
637
+ if (!isIntentionalTarget) {
638
+ const postStabilityUrl = this.page.url();
639
+ const postStabilityRedirect =
640
+ isAuthRedirectUrl(postStabilityUrl, this._customAuthPatterns) ||
641
+ this._authResponseDetected;
642
+ if (postStabilityRedirect) {
643
+ const errorMsg = `Auth redirect detected after page load: navigated to ${postStabilityUrl}. Session may be expired. Re-run \`reshot record\` to refresh session.`;
644
+ this.logger(chalk.red(` ✖ ${errorMsg}`));
645
+ throw new Error(errorMsg);
646
+ }
647
+ // Also check DOM for login forms (SPA may render login UI without changing URL)
648
+ const hasLoginForm = await this.page.evaluate(() => {
649
+ const h = document.querySelector("h1, h2");
650
+ return h && /sign\s*in|log\s*in/i.test(h.textContent);
651
+ }).catch(() => false);
652
+ if (hasLoginForm) {
653
+ const errorMsg = `Login form detected after page load at ${postStabilityUrl}. Session may be expired. Re-run \`reshot record\` to refresh session.`;
654
+ this.logger(chalk.red(` ✖ ${errorMsg}`));
655
+ throw new Error(errorMsg);
656
+ }
657
+ this._authResponseDetected = false;
658
+ }
659
+
626
660
  // Additional wait for theme/variants to fully apply
627
661
  // This handles CSS transitions and async re-renders
628
662
  if (this.variantConfig && this.variantConfig.injections?.length > 0) {
@@ -1107,10 +1141,50 @@ class CaptureEngine {
1107
1141
  // Write the final buffer to file
1108
1142
  await fs.writeFile(outputPath, finalBuffer);
1109
1143
 
1144
+ // ── DOM scene capture (sidecar MHTML) ──────────────────────────────
1145
+ // Capture a self-contained MHTML bundle of the page at the same moment
1146
+ // as the PNG. The bundle re-renders in any Chromium browser without
1147
+ // network access and is the source of truth for variations: marketing
1148
+ // can mutate the captured DOM (swap copy, hide chrome, recrop, rebrand
1149
+ // tenant names) and render new outputs without re-running Playwright
1150
+ // against the live app. Opt out per-scenario or globally via
1151
+ // reshot.config.json -> { domScene: false }.
1152
+ //
1153
+ // The MHTML capture is best-effort — failures are logged but do not
1154
+ // fail the scenario, since the primary PNG already succeeded.
1155
+ let domScenePath = null;
1156
+ let domSceneBytes = null;
1157
+ if (this.domSceneEnabled) {
1158
+ try {
1159
+ const cdp = await this.page.context().newCDPSession(this.page);
1160
+ const { data: mhtml } = await cdp.send("Page.captureSnapshot", {
1161
+ format: "mhtml",
1162
+ });
1163
+ domScenePath = outputPath.replace(/\.png$/i, ".mhtml");
1164
+ await fs.writeFile(domScenePath, mhtml);
1165
+ domSceneBytes = Buffer.byteLength(mhtml, "utf8");
1166
+ this.logger(
1167
+ chalk.gray(
1168
+ ` ✓ DOM scene: ${path.basename(domScenePath)} (${(domSceneBytes / 1024).toFixed(0)} KB)`,
1169
+ ),
1170
+ );
1171
+ } catch (err) {
1172
+ this.logger(
1173
+ chalk.yellow(
1174
+ ` ⚠ DOM scene capture skipped: ${err.message || err}`,
1175
+ ),
1176
+ );
1177
+ domScenePath = null;
1178
+ domSceneBytes = null;
1179
+ }
1180
+ }
1181
+
1110
1182
  // Record asset metadata
1111
1183
  this.capturedAssets.push({
1112
1184
  name,
1113
1185
  path: outputPath,
1186
+ domScenePath,
1187
+ domSceneBytes,
1114
1188
  description,
1115
1189
  capturedAt: new Date().toISOString(),
1116
1190
  viewport: this.viewport,