@reshotdev/screenshot 0.0.1-beta.11 → 0.0.1-beta.13

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 (66) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +84 -51
  3. package/package.json +20 -16
  4. package/src/commands/auth.js +38 -8
  5. package/src/commands/capture-dom.js +50 -0
  6. package/src/commands/compose.js +220 -0
  7. package/src/commands/doctor-target.js +36 -4
  8. package/src/commands/drifts.js +13 -1
  9. package/src/commands/publish.js +137 -12
  10. package/src/commands/pull.js +13 -8
  11. package/src/commands/refresh.js +166 -0
  12. package/src/commands/setup-wizard.js +35 -2
  13. package/src/commands/status.js +22 -2
  14. package/src/commands/variation.js +194 -0
  15. package/src/index.js +189 -47
  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 +73 -0
  21. package/src/lib/capture-script-runner.js +280 -134
  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 +5 -5
  28. package/src/lib/dom-capture.js +64 -0
  29. package/src/lib/output-path-template.js +3 -3
  30. package/src/lib/record-clip.js +83 -3
  31. package/src/lib/record-config.js +0 -4
  32. package/src/lib/resolve-targets.js +60 -0
  33. package/src/lib/run-manifest.js +45 -0
  34. package/src/lib/storage-providers.js +1 -1
  35. package/src/lib/style-engine.js +5 -5
  36. package/src/lib/ui-api-helpers.js +118 -0
  37. package/src/lib/ui-api.js +28 -820
  38. package/src/lib/ui-asset-cleanup.js +62 -0
  39. package/src/lib/ui-output-versions.js +165 -0
  40. package/src/lib/ui-recorder-routes.js +341 -0
  41. package/src/lib/ui-scenario-metadata.js +161 -0
  42. package/vendor/compose/dist/auto-update.cjs +5544 -0
  43. package/vendor/compose/dist/auto-update.mjs +5518 -0
  44. package/vendor/compose/dist/capture.cjs +1450 -0
  45. package/vendor/compose/dist/capture.mjs +1416 -0
  46. package/vendor/compose/dist/eligibility.cjs +5331 -0
  47. package/vendor/compose/dist/eligibility.mjs +5313 -0
  48. package/vendor/compose/dist/index.cjs +2046 -0
  49. package/vendor/compose/dist/index.mjs +1997 -0
  50. package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
  51. package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
  52. package/vendor/compose/dist/jsx-runtime.cjs +58 -0
  53. package/vendor/compose/dist/jsx-runtime.mjs +31 -0
  54. package/vendor/compose/dist/render.cjs +558 -0
  55. package/vendor/compose/dist/render.mjs +515 -0
  56. package/vendor/compose/dist/verify-cli.cjs +3806 -0
  57. package/vendor/compose/dist/verify-cli.mjs +3812 -0
  58. package/vendor/compose/dist/verify.cjs +3880 -0
  59. package/vendor/compose/dist/verify.mjs +3858 -0
  60. package/web/manager/dist/assets/{index-D2qqcFNN.js → index-D0S2otug.js} +56 -56
  61. package/web/manager/dist/index.html +1 -1
  62. package/src/commands/ci-run.js +0 -178
  63. package/src/commands/ci-setup.js +0 -288
  64. package/src/commands/ingest.js +0 -458
  65. package/src/commands/setup.js +0 -165
  66. 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
+ };
@@ -145,6 +145,13 @@ class CaptureEngine {
145
145
  // Style configuration (image beautification post-capture)
146
146
  this.styleConfig = options.styleConfig || null;
147
147
 
148
+ // DOM scene (MHTML) capture toggle — emits a self-contained Chromium
149
+ // MHTML bundle alongside each PNG so variations can be rendered from
150
+ // the captured DOM without re-running the live app. Defaults to ON;
151
+ // opt out via reshot.config.json -> { domScene: false } or per-scenario
152
+ // -> { domScene: false }.
153
+ this.domSceneEnabled = options.domScene !== false;
154
+
148
155
  // Legacy support for old variant format
149
156
  if (!this.variantConfig && options.variant) {
150
157
  this.variantConfig = this._convertLegacyVariant(options.variant);
@@ -623,6 +630,32 @@ class CaptureEngine {
623
630
  // Wait for network to settle
624
631
  await this._waitForStability();
625
632
 
633
+ // Post-stability auth redirect check: catches SPA redirects that happen
634
+ // after client-side JS has executed (the pre-stability check at domcontentloaded
635
+ // may miss these since JS hasn't finished routing yet)
636
+ if (!isIntentionalTarget) {
637
+ const postStabilityUrl = this.page.url();
638
+ const postStabilityRedirect =
639
+ isAuthRedirectUrl(postStabilityUrl, this._customAuthPatterns) ||
640
+ this._authResponseDetected;
641
+ if (postStabilityRedirect) {
642
+ const errorMsg = `Auth redirect detected after page load: navigated to ${postStabilityUrl}. Session may be expired. Re-run \`reshot record\` to refresh session.`;
643
+ this.logger(chalk.red(` ✖ ${errorMsg}`));
644
+ throw new Error(errorMsg);
645
+ }
646
+ // Also check DOM for login forms (SPA may render login UI without changing URL)
647
+ const hasLoginForm = await this.page.evaluate(() => {
648
+ const h = document.querySelector("h1, h2");
649
+ return h && /sign\s*in|log\s*in/i.test(h.textContent);
650
+ }).catch(() => false);
651
+ if (hasLoginForm) {
652
+ const errorMsg = `Login form detected after page load at ${postStabilityUrl}. Session may be expired. Re-run \`reshot record\` to refresh session.`;
653
+ this.logger(chalk.red(` ✖ ${errorMsg}`));
654
+ throw new Error(errorMsg);
655
+ }
656
+ this._authResponseDetected = false;
657
+ }
658
+
626
659
  // Additional wait for theme/variants to fully apply
627
660
  // This handles CSS transitions and async re-renders
628
661
  if (this.variantConfig && this.variantConfig.injections?.length > 0) {
@@ -1107,10 +1140,50 @@ class CaptureEngine {
1107
1140
  // Write the final buffer to file
1108
1141
  await fs.writeFile(outputPath, finalBuffer);
1109
1142
 
1143
+ // ── DOM scene capture (sidecar MHTML) ──────────────────────────────
1144
+ // Capture a self-contained MHTML bundle of the page at the same moment
1145
+ // as the PNG. The bundle re-renders in any Chromium browser without
1146
+ // network access and is the source of truth for variations: marketing
1147
+ // can mutate the captured DOM (swap copy, hide chrome, recrop, rebrand
1148
+ // tenant names) and render new outputs without re-running Playwright
1149
+ // against the live app. Opt out per-scenario or globally via
1150
+ // reshot.config.json -> { domScene: false }.
1151
+ //
1152
+ // The MHTML capture is best-effort — failures are logged but do not
1153
+ // fail the scenario, since the primary PNG already succeeded.
1154
+ let domScenePath = null;
1155
+ let domSceneBytes = null;
1156
+ if (this.domSceneEnabled) {
1157
+ try {
1158
+ const cdp = await this.page.context().newCDPSession(this.page);
1159
+ const { data: mhtml } = await cdp.send("Page.captureSnapshot", {
1160
+ format: "mhtml",
1161
+ });
1162
+ domScenePath = outputPath.replace(/\.png$/i, ".mhtml");
1163
+ await fs.writeFile(domScenePath, mhtml);
1164
+ domSceneBytes = Buffer.byteLength(mhtml, "utf8");
1165
+ this.logger(
1166
+ chalk.gray(
1167
+ ` ✓ DOM scene: ${path.basename(domScenePath)} (${(domSceneBytes / 1024).toFixed(0)} KB)`,
1168
+ ),
1169
+ );
1170
+ } catch (err) {
1171
+ this.logger(
1172
+ chalk.yellow(
1173
+ ` ⚠ DOM scene capture skipped: ${err.message || err}`,
1174
+ ),
1175
+ );
1176
+ domScenePath = null;
1177
+ domSceneBytes = null;
1178
+ }
1179
+ }
1180
+
1110
1181
  // Record asset metadata
1111
1182
  this.capturedAssets.push({
1112
1183
  name,
1113
1184
  path: outputPath,
1185
+ domScenePath,
1186
+ domSceneBytes,
1114
1187
  description,
1115
1188
  capturedAt: new Date().toISOString(),
1116
1189
  viewport: this.viewport,