@reshotdev/screenshot 0.0.1-beta.2 ā 0.0.1-beta.21
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 +1 -1
- package/README.md +138 -47
- package/package.json +27 -16
- package/src/commands/auth.js +159 -30
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/certify.js +62 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-release.js +74 -0
- package/src/commands/doctor-target.js +108 -0
- package/src/commands/drifts.js +16 -69
- package/src/commands/import-tests.js +13 -13
- package/src/commands/init.js +16 -277
- package/src/commands/publish.js +484 -257
- package/src/commands/pull.js +302 -35
- package/src/commands/refresh.js +166 -0
- package/src/commands/run.js +292 -12
- package/src/commands/setup-wizard.js +348 -496
- package/src/commands/status.js +334 -126
- package/src/commands/sync.js +28 -236
- package/src/commands/ui.js +1 -1
- package/src/commands/variation.js +194 -0
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +383 -118
- package/src/lib/api-client.js +172 -60
- package/src/lib/auto-update/refresh.js +598 -0
- package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
- package/src/lib/auto-update/spec.js +89 -0
- package/src/lib/capture-engine.js +179 -9
- package/src/lib/capture-script-runner.js +639 -214
- package/src/lib/certification.js +887 -0
- package/src/lib/compose-context.js +156 -0
- package/src/lib/compose-pack.js +42 -0
- package/src/lib/compose-runtime.js +34 -0
- package/src/lib/compose-upload.js +142 -0
- package/src/lib/config.js +186 -81
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/output-path-template.js +3 -3
- package/src/lib/record-cdp.js +288 -16
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +1 -5
- package/src/lib/release-doctor.js +321 -0
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +148 -0
- package/src/lib/standalone-mode.js +1 -1
- package/src/lib/storage-providers.js +5 -5
- package/src/lib/style-engine.js +5 -5
- package/src/lib/target-contract.js +292 -0
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +31 -824
- package/src/lib/ui-asset-cleanup.js +62 -0
- package/src/lib/ui-output-versions.js +165 -0
- package/src/lib/ui-recorder-routes.js +341 -0
- package/src/lib/ui-scenario-metadata.js +161 -0
- package/vendor/compose/dist/auto-update.cjs +5544 -0
- package/vendor/compose/dist/auto-update.mjs +5518 -0
- package/vendor/compose/dist/capture.cjs +1450 -0
- package/vendor/compose/dist/capture.mjs +1416 -0
- package/vendor/compose/dist/eligibility.cjs +5331 -0
- package/vendor/compose/dist/eligibility.mjs +5313 -0
- package/vendor/compose/dist/index.cjs +2046 -0
- package/vendor/compose/dist/index.mjs +1997 -0
- package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
- package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
- package/vendor/compose/dist/jsx-runtime.cjs +58 -0
- package/vendor/compose/dist/jsx-runtime.mjs +31 -0
- package/vendor/compose/dist/render.cjs +558 -0
- package/vendor/compose/dist/render.mjs +515 -0
- package/vendor/compose/dist/verify-cli.cjs +3806 -0
- package/vendor/compose/dist/verify-cli.mjs +3812 -0
- package/vendor/compose/dist/verify.cjs +3880 -0
- package/vendor/compose/dist/verify.mjs +3858 -0
- package/web/manager/dist/assets/index-D0S2otug.js +507 -0
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ci-run.js +0 -123
- package/src/commands/ci-setup.js +0 -288
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -137
- package/src/commands/validate-docs.js +0 -529
- package/src/lib/playwright-runner.js +0 -252
- package/web/manager/dist/assets/index--ZgioErz.js +0 -507
package/src/commands/sync.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// sync.js - Upload Playwright traces
|
|
2
|
-
// This is the renamed/enhanced version of ingest.js
|
|
3
|
-
// Implements the "Smart Handoff" protocol from the DocSync specification
|
|
1
|
+
// sync.js - Upload Playwright traces to Reshot platform
|
|
4
2
|
|
|
5
3
|
const chalk = require("chalk");
|
|
6
4
|
const crypto = require("crypto");
|
|
@@ -9,127 +7,12 @@ const path = require("path");
|
|
|
9
7
|
const { execSync } = require("child_process");
|
|
10
8
|
const config = require("../lib/config");
|
|
11
9
|
const apiClient = require("../lib/api-client");
|
|
12
|
-
const { hashFile } = require("../lib/hash");
|
|
13
10
|
const pkg = require("../../package.json");
|
|
14
11
|
|
|
15
12
|
// File extension allowlists
|
|
16
13
|
const TRACE_EXTENSIONS = [".zip"];
|
|
17
|
-
const DOC_EXTENSIONS = [".md", ".mdx"];
|
|
18
|
-
const MAX_DOC_SIZE = 5 * 1024 * 1024; // 5MB per doc file
|
|
19
14
|
const MAX_TRACE_SIZE = 100 * 1024 * 1024; // 100MB per trace
|
|
20
15
|
|
|
21
|
-
/**
|
|
22
|
-
* Parse frontmatter from a markdown file
|
|
23
|
-
*/
|
|
24
|
-
function parseFrontmatter(content) {
|
|
25
|
-
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
26
|
-
const match = content.match(frontmatterRegex);
|
|
27
|
-
|
|
28
|
-
if (!match) return { frontmatter: {}, content };
|
|
29
|
-
|
|
30
|
-
const frontmatter = {};
|
|
31
|
-
const lines = match[1].split("\n");
|
|
32
|
-
|
|
33
|
-
for (const line of lines) {
|
|
34
|
-
const colonIndex = line.indexOf(":");
|
|
35
|
-
if (colonIndex > 0) {
|
|
36
|
-
const key = line.slice(0, colonIndex).trim();
|
|
37
|
-
let value = line.slice(colonIndex + 1).trim();
|
|
38
|
-
if (
|
|
39
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
40
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
41
|
-
) {
|
|
42
|
-
value = value.slice(1, -1);
|
|
43
|
-
}
|
|
44
|
-
frontmatter[key] = value;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
frontmatter,
|
|
50
|
-
content: content.slice(match[0].length).trim(),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Discover documentation files based on config
|
|
56
|
-
*/
|
|
57
|
-
async function discoverDocumentation(docConfig, projectRoot) {
|
|
58
|
-
const files = [];
|
|
59
|
-
const root = path.resolve(projectRoot, docConfig.root || "./docs");
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(root)) {
|
|
62
|
-
return files;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const include = docConfig.include || ["**/*.md", "**/*.mdx"];
|
|
66
|
-
const exclude = docConfig.exclude || ["**/_*.mdx", "node_modules"];
|
|
67
|
-
const mappings = docConfig.mappings || {};
|
|
68
|
-
|
|
69
|
-
function walkDir(dir, relativePath = "") {
|
|
70
|
-
const items = fs.readdirSync(dir);
|
|
71
|
-
|
|
72
|
-
for (const item of items) {
|
|
73
|
-
const fullPath = path.join(dir, item);
|
|
74
|
-
const relPath = path.join(relativePath, item);
|
|
75
|
-
const stat = fs.statSync(fullPath);
|
|
76
|
-
|
|
77
|
-
const shouldExclude = exclude.some((pattern) => {
|
|
78
|
-
if (pattern.includes("*")) {
|
|
79
|
-
const regex = new RegExp(
|
|
80
|
-
pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"),
|
|
81
|
-
);
|
|
82
|
-
return regex.test(relPath);
|
|
83
|
-
}
|
|
84
|
-
return relPath.includes(pattern) || item === pattern;
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (shouldExclude) continue;
|
|
88
|
-
|
|
89
|
-
if (stat.isDirectory()) {
|
|
90
|
-
walkDir(fullPath, relPath);
|
|
91
|
-
} else {
|
|
92
|
-
const ext = path.extname(item).toLowerCase();
|
|
93
|
-
if (DOC_EXTENSIONS.includes(ext)) {
|
|
94
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
95
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
96
|
-
|
|
97
|
-
const journeyKey =
|
|
98
|
-
frontmatter.reshot_journey ||
|
|
99
|
-
mappings[relPath] ||
|
|
100
|
-
mappings[fullPath];
|
|
101
|
-
|
|
102
|
-
if (journeyKey) {
|
|
103
|
-
const fileSize = stat.size;
|
|
104
|
-
|
|
105
|
-
if (fileSize > MAX_DOC_SIZE) {
|
|
106
|
-
console.log(
|
|
107
|
-
chalk.yellow(` ā Skipping ${relPath}: exceeds size limit`),
|
|
108
|
-
);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
files.push({
|
|
113
|
-
path: fullPath,
|
|
114
|
-
relativePath: relPath,
|
|
115
|
-
journeyKey,
|
|
116
|
-
contentHash: crypto
|
|
117
|
-
.createHash("sha256")
|
|
118
|
-
.update(content)
|
|
119
|
-
.digest("hex"),
|
|
120
|
-
size: fileSize,
|
|
121
|
-
frontmatter,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
walkDir(root);
|
|
130
|
-
return files;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
16
|
/**
|
|
134
17
|
* Extract journey key from Playwright test-results directory name
|
|
135
18
|
* Playwright creates directories like: "test-name-chromium" or "describe-test-name-chromium"
|
|
@@ -191,11 +74,6 @@ function extractJourneyKey(dirName, journeyMappings = {}) {
|
|
|
191
74
|
}
|
|
192
75
|
|
|
193
76
|
// Check if the key parts of the test name appear in both
|
|
194
|
-
// Match "student-login-successfully" to pattern "should-complete-student-login-successfully"
|
|
195
|
-
const patternStr = pattern.toLowerCase();
|
|
196
|
-
const normalizedStr = normalized;
|
|
197
|
-
|
|
198
|
-
// Extract meaningful words (4+ chars) and check overlap
|
|
199
77
|
const meaningfulFromPattern = patternParts.filter((p) => p.length >= 4);
|
|
200
78
|
const meaningfulFromDir = normalizedParts.filter((p) => p.length >= 4);
|
|
201
79
|
|
|
@@ -323,28 +201,24 @@ function getGitMetadata() {
|
|
|
323
201
|
* Main sync command
|
|
324
202
|
* @param {Object} options - Command options
|
|
325
203
|
* @param {string} options.traceDir - Override trace directory
|
|
326
|
-
* @param {boolean} options.traces - Sync traces only
|
|
327
|
-
* @param {boolean} options.docs - Sync documentation only
|
|
328
204
|
* @param {boolean} options.dryRun - Preview without uploading
|
|
329
205
|
* @param {boolean} options.verbose - Show detailed output
|
|
330
206
|
*/
|
|
331
207
|
async function syncCommand(options = {}) {
|
|
332
208
|
const {
|
|
333
209
|
traceDir: traceDirOverride,
|
|
334
|
-
traces: tracesOnly = false,
|
|
335
|
-
docs: docsOnly = false,
|
|
336
210
|
dryRun = false,
|
|
337
211
|
verbose = false,
|
|
338
212
|
} = options;
|
|
339
213
|
|
|
340
214
|
console.log(chalk.cyan.bold("\nš Reshot Sync\n"));
|
|
341
215
|
|
|
342
|
-
// Load configuration
|
|
343
|
-
let
|
|
216
|
+
// Load configuration
|
|
217
|
+
let reshotConfig;
|
|
344
218
|
try {
|
|
345
|
-
|
|
219
|
+
reshotConfig = config.readConfigLenient();
|
|
346
220
|
} catch (error) {
|
|
347
|
-
console.error(chalk.red("ā No
|
|
221
|
+
console.error(chalk.red("ā No reshot.config.json found."));
|
|
348
222
|
console.log(
|
|
349
223
|
chalk.gray(" Run"),
|
|
350
224
|
chalk.cyan("reshot setup"),
|
|
@@ -361,7 +235,7 @@ async function syncCommand(options = {}) {
|
|
|
361
235
|
settings = null;
|
|
362
236
|
}
|
|
363
237
|
|
|
364
|
-
const projectId =
|
|
238
|
+
const projectId = reshotConfig.projectId || settings?.projectId;
|
|
365
239
|
const apiKey = settings?.apiKey;
|
|
366
240
|
|
|
367
241
|
if (!projectId || !apiKey) {
|
|
@@ -374,99 +248,55 @@ async function syncCommand(options = {}) {
|
|
|
374
248
|
process.exit(1);
|
|
375
249
|
}
|
|
376
250
|
|
|
377
|
-
const features = docSyncConfig._metadata?.features || {
|
|
378
|
-
visuals: true,
|
|
379
|
-
docsync: true,
|
|
380
|
-
};
|
|
381
251
|
const projectRoot = process.cwd();
|
|
382
252
|
|
|
383
|
-
// Determine what to sync
|
|
384
|
-
const syncTraces = !docsOnly && features.visuals !== false;
|
|
385
|
-
const syncDocs =
|
|
386
|
-
!tracesOnly && features.docsync !== false && docSyncConfig.documentation;
|
|
387
|
-
|
|
388
253
|
if (dryRun) {
|
|
389
254
|
console.log(chalk.yellow("DRY RUN - No files will be uploaded\n"));
|
|
390
255
|
}
|
|
391
256
|
|
|
392
|
-
let traceFiles = [];
|
|
393
|
-
let docFiles = [];
|
|
394
|
-
|
|
395
257
|
// ========================================
|
|
396
258
|
// PHASE 1: Discover Traces
|
|
397
259
|
// ========================================
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const journeyMappings = docSyncConfig.visuals?.journeyMappings || {};
|
|
403
|
-
|
|
404
|
-
console.log(chalk.gray(`Scanning traces: ${traceDir}`));
|
|
405
|
-
traceFiles = await discoverTraces(resolvedTraceDir, journeyMappings);
|
|
406
|
-
|
|
407
|
-
if (traceFiles.length === 0) {
|
|
408
|
-
console.log(chalk.yellow(" No trace files found"));
|
|
409
|
-
if (!fs.existsSync(resolvedTraceDir)) {
|
|
410
|
-
console.log(
|
|
411
|
-
chalk.gray(" Run your Playwright tests first: npx playwright test"),
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
} else {
|
|
415
|
-
console.log(chalk.green(` Found ${traceFiles.length} trace file(s)`));
|
|
416
|
-
if (verbose) {
|
|
417
|
-
for (const trace of traceFiles) {
|
|
418
|
-
console.log(
|
|
419
|
-
chalk.gray(` ā ${trace.parentDir || trace.relativePath}`),
|
|
420
|
-
);
|
|
421
|
-
console.log(chalk.gray(` journey: ${trace.journeyKey}`));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
console.log();
|
|
426
|
-
}
|
|
260
|
+
const traceDir =
|
|
261
|
+
traceDirOverride || reshotConfig.visuals?.traceDir || "./test-results";
|
|
262
|
+
const resolvedTraceDir = path.resolve(projectRoot, traceDir);
|
|
263
|
+
const journeyMappings = reshotConfig.visuals?.journeyMappings || {};
|
|
427
264
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// ========================================
|
|
431
|
-
if (syncDocs) {
|
|
432
|
-
console.log(
|
|
433
|
-
chalk.gray(`Scanning docs: ${docSyncConfig.documentation.root}`),
|
|
434
|
-
);
|
|
435
|
-
docFiles = await discoverDocumentation(
|
|
436
|
-
docSyncConfig.documentation,
|
|
437
|
-
projectRoot,
|
|
438
|
-
);
|
|
265
|
+
console.log(chalk.gray(`Scanning traces: ${traceDir}`));
|
|
266
|
+
const traceFiles = await discoverTraces(resolvedTraceDir, journeyMappings);
|
|
439
267
|
|
|
440
|
-
|
|
441
|
-
|
|
268
|
+
if (traceFiles.length === 0) {
|
|
269
|
+
console.log(chalk.yellow(" No trace files found"));
|
|
270
|
+
if (!fs.existsSync(resolvedTraceDir)) {
|
|
442
271
|
console.log(
|
|
443
|
-
chalk.gray("
|
|
272
|
+
chalk.gray(" Run your Playwright tests first: npx playwright test"),
|
|
444
273
|
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
274
|
+
}
|
|
275
|
+
console.log();
|
|
276
|
+
} else {
|
|
277
|
+
console.log(chalk.green(` Found ${traceFiles.length} trace file(s)`));
|
|
278
|
+
if (verbose) {
|
|
279
|
+
for (const trace of traceFiles) {
|
|
280
|
+
console.log(
|
|
281
|
+
chalk.gray(` ā ${trace.parentDir || trace.relativePath}`),
|
|
282
|
+
);
|
|
283
|
+
console.log(chalk.gray(` journey: ${trace.journeyKey}`));
|
|
453
284
|
}
|
|
454
285
|
}
|
|
455
286
|
console.log();
|
|
456
287
|
}
|
|
457
288
|
|
|
458
289
|
// ========================================
|
|
459
|
-
// PHASE
|
|
290
|
+
// PHASE 2: Upload to Platform
|
|
460
291
|
// ========================================
|
|
461
292
|
if (dryRun) {
|
|
462
293
|
console.log(chalk.cyan("āāā Dry Run Summary āāā\n"));
|
|
463
294
|
console.log(chalk.gray(` Traces: ${traceFiles.length} files`));
|
|
464
|
-
console.log(chalk.gray(` Docs: ${docFiles.length} files`));
|
|
465
295
|
console.log(chalk.yellow("\nNo files uploaded (dry run).\n"));
|
|
466
296
|
return;
|
|
467
297
|
}
|
|
468
298
|
|
|
469
|
-
if (traceFiles.length === 0
|
|
299
|
+
if (traceFiles.length === 0) {
|
|
470
300
|
console.log(chalk.yellow("Nothing to sync.\n"));
|
|
471
301
|
return;
|
|
472
302
|
}
|
|
@@ -476,12 +306,6 @@ async function syncCommand(options = {}) {
|
|
|
476
306
|
try {
|
|
477
307
|
// Initialize sync job with manifest
|
|
478
308
|
const manifest = {
|
|
479
|
-
docs: docFiles.map((d) => ({
|
|
480
|
-
relativePath: d.relativePath,
|
|
481
|
-
journeyKey: d.journeyKey,
|
|
482
|
-
contentHash: d.contentHash,
|
|
483
|
-
size: d.size,
|
|
484
|
-
})),
|
|
485
309
|
traces: traceFiles.map((t) => ({
|
|
486
310
|
filename: t.filename,
|
|
487
311
|
journeyKey: t.journeyKey,
|
|
@@ -545,31 +369,6 @@ async function syncCommand(options = {}) {
|
|
|
545
369
|
}
|
|
546
370
|
}
|
|
547
371
|
|
|
548
|
-
// Upload docs to presigned URLs
|
|
549
|
-
let docsUploaded = 0;
|
|
550
|
-
for (const doc of docFiles) {
|
|
551
|
-
if (skippedFiles.includes(doc.contentHash)) {
|
|
552
|
-
if (verbose) {
|
|
553
|
-
console.log(chalk.gray(` ā ${doc.relativePath} (cached)`));
|
|
554
|
-
}
|
|
555
|
-
docsUploaded++;
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const presigned = presignedUrls[doc.contentHash];
|
|
560
|
-
if (presigned) {
|
|
561
|
-
const content = fs.readFileSync(doc.path, "utf-8");
|
|
562
|
-
await apiClient.uploadToPresignedUrl(presigned.url, content, {
|
|
563
|
-
contentType: presigned.contentType || "text/markdown",
|
|
564
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
565
|
-
});
|
|
566
|
-
docsUploaded++;
|
|
567
|
-
if (verbose) {
|
|
568
|
-
console.log(chalk.gray(` ā ${doc.relativePath}`));
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
372
|
// Get git metadata
|
|
574
373
|
const git = getGitMetadata();
|
|
575
374
|
|
|
@@ -579,12 +378,6 @@ async function syncCommand(options = {}) {
|
|
|
579
378
|
{
|
|
580
379
|
projectId,
|
|
581
380
|
uploadResults: {
|
|
582
|
-
docs: docFiles.map((d) => ({
|
|
583
|
-
relativePath: d.relativePath,
|
|
584
|
-
journeyKey: d.journeyKey,
|
|
585
|
-
storageKey:
|
|
586
|
-
presignedUrls[d.contentHash]?.storageKey || d.contentHash,
|
|
587
|
-
})),
|
|
588
381
|
traces: traceFiles.map((t) => ({
|
|
589
382
|
filename: t.filename,
|
|
590
383
|
journeyKey: t.journeyKey,
|
|
@@ -605,7 +398,6 @@ async function syncCommand(options = {}) {
|
|
|
605
398
|
|
|
606
399
|
console.log(chalk.green(`\nā Sync complete!`));
|
|
607
400
|
console.log(chalk.gray(` Traces: ${tracesUploaded} uploaded`));
|
|
608
|
-
console.log(chalk.gray(` Docs: ${docsUploaded} uploaded`));
|
|
609
401
|
console.log();
|
|
610
402
|
console.log(chalk.gray(" View status:"), chalk.cyan("reshot status"));
|
|
611
403
|
console.log(chalk.gray(" Check drifts:"), chalk.cyan("reshot drifts\n"));
|
package/src/commands/ui.js
CHANGED
|
@@ -127,7 +127,7 @@ module.exports = async function uiCommand(options = {}) {
|
|
|
127
127
|
} else {
|
|
128
128
|
console.log(
|
|
129
129
|
chalk.yellow("Info:"),
|
|
130
|
-
"No
|
|
130
|
+
"No reshot.config.json found. Use the UI to pull config from platform or create a new one."
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// variation.js - Render variations from a captured DOM scene.
|
|
2
|
+
//
|
|
3
|
+
// Workflow:
|
|
4
|
+
// 1. Resolve the source MHTML ā either from the local .reshot/output tree
|
|
5
|
+
// or by downloading from the platform CDN (R2 public URL stored on
|
|
6
|
+
// the VisualVersion as domSceneS3Path).
|
|
7
|
+
// 2. Open the MHTML in a fresh headless Chromium.
|
|
8
|
+
// 3. Apply the variation manifest (JS mutations + viewport).
|
|
9
|
+
// 4. Screenshot.
|
|
10
|
+
//
|
|
11
|
+
// This is the v1 marketing-operator entry point. The dashboard UI on top
|
|
12
|
+
// of it is the next deliverable; the CLI exists so the loop is usable
|
|
13
|
+
// the moment Phase 1 ships.
|
|
14
|
+
//
|
|
15
|
+
// Manifest format (JSON):
|
|
16
|
+
//
|
|
17
|
+
// {
|
|
18
|
+
// "viewport": { "width": 1440, "height": 900, "deviceScaleFactor": 2 },
|
|
19
|
+
// "colorScheme": "light" | "dark",
|
|
20
|
+
// "mutations": [
|
|
21
|
+
// { "remove": "aside, nav" }, // delete elements
|
|
22
|
+
// { "replaceText": [
|
|
23
|
+
// { "find": "ari@tempo.example", "with": "alice@acme.com" }
|
|
24
|
+
// ]},
|
|
25
|
+
// { "limit": { "selector": "tbody tr", "count": 5 } }, // keep first N
|
|
26
|
+
// { "setText": { "selector": "[data-author]", "text": "alice@acme.com" } },
|
|
27
|
+
// { "setAttr": { "selector": "img.logo", "name": "src", "value": "..." } },
|
|
28
|
+
// { "evaluate": "/* arbitrary page.evaluate script */" }
|
|
29
|
+
// ]
|
|
30
|
+
// }
|
|
31
|
+
|
|
32
|
+
const fs = require("fs-extra");
|
|
33
|
+
const path = require("node:path");
|
|
34
|
+
const chalk = require("chalk");
|
|
35
|
+
const { chromium } = require("playwright");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Runs in the browser context. Pure function ā no closures over Node
|
|
39
|
+
* state. `manifestJson` is the only input. Keep this synchronous; if
|
|
40
|
+
* the page has React rehydration scripts, give them time before calling
|
|
41
|
+
* by waiting in the caller, then apply mutations as the *last* DOM
|
|
42
|
+
* write so they aren't immediately overwritten.
|
|
43
|
+
*/
|
|
44
|
+
function applyMutationsInBrowser(manifestJson) {
|
|
45
|
+
const m = JSON.parse(manifestJson);
|
|
46
|
+
for (const mut of m.mutations || []) {
|
|
47
|
+
if (mut.remove) {
|
|
48
|
+
document.querySelectorAll(mut.remove).forEach((el) => el.remove());
|
|
49
|
+
}
|
|
50
|
+
if (mut.replaceText) {
|
|
51
|
+
for (const r of mut.replaceText) {
|
|
52
|
+
document.body.innerHTML = document.body.innerHTML
|
|
53
|
+
.split(r.find)
|
|
54
|
+
.join(r.with);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (mut.limit) {
|
|
58
|
+
const els = document.querySelectorAll(mut.limit.selector);
|
|
59
|
+
els.forEach((el, i) => {
|
|
60
|
+
if (i >= mut.limit.count) el.remove();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (mut.setText) {
|
|
64
|
+
document.querySelectorAll(mut.setText.selector).forEach((el) => {
|
|
65
|
+
el.textContent = mut.setText.text;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (mut.setAttr) {
|
|
69
|
+
document.querySelectorAll(mut.setAttr.selector).forEach((el) => {
|
|
70
|
+
el.setAttribute(mut.setAttr.name, mut.setAttr.value);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (mut.evaluate) {
|
|
74
|
+
// eslint-disable-next-line no-new-func
|
|
75
|
+
new Function(mut.evaluate)();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveSourceMhtml({ source, scenarioKey, captureKey, theme }) {
|
|
81
|
+
// 1) Explicit HTTPS URL ā download to tmp, return local path. The local
|
|
82
|
+
// file MUST end in .mhtml ā Chromium detects MHTML by extension, not by
|
|
83
|
+
// sniffing magic bytes, and our CDN serves under .related (the extension
|
|
84
|
+
// derived from multipart/related contentType).
|
|
85
|
+
if (source && /^https?:\/\//.test(source)) {
|
|
86
|
+
const tmpDir = path.join(require("os").tmpdir(), "reshot-variation");
|
|
87
|
+
fs.ensureDirSync(tmpDir);
|
|
88
|
+
const localPath = path.join(tmpDir, `${Date.now()}-variation.mhtml`);
|
|
89
|
+
const res = await fetch(source);
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new Error(`Failed to fetch source MHTML: ${res.status} ${res.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
94
|
+
await fs.writeFile(localPath, buf);
|
|
95
|
+
return { kind: "remote", path: localPath };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2) Explicit local path
|
|
99
|
+
if (source && fs.existsSync(source)) {
|
|
100
|
+
return { kind: "local", path: source };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3) Look in .reshot/output tree
|
|
104
|
+
if (scenarioKey && captureKey) {
|
|
105
|
+
const root = path.join(process.cwd(), ".reshot", "output", scenarioKey);
|
|
106
|
+
if (fs.existsSync(root)) {
|
|
107
|
+
const runs = fs.readdirSync(root)
|
|
108
|
+
.filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d))
|
|
109
|
+
.sort()
|
|
110
|
+
.reverse();
|
|
111
|
+
for (const run of runs) {
|
|
112
|
+
const candidate = path.join(
|
|
113
|
+
root,
|
|
114
|
+
run,
|
|
115
|
+
theme ? `theme-${theme}` : "default",
|
|
116
|
+
`${captureKey}.mhtml`,
|
|
117
|
+
);
|
|
118
|
+
if (fs.existsSync(candidate)) {
|
|
119
|
+
return { kind: "local", path: candidate };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function variationCommand(options) {
|
|
129
|
+
const {
|
|
130
|
+
source,
|
|
131
|
+
scenario,
|
|
132
|
+
capture,
|
|
133
|
+
theme = "light",
|
|
134
|
+
manifest: manifestPath,
|
|
135
|
+
output,
|
|
136
|
+
headless = true,
|
|
137
|
+
} = options;
|
|
138
|
+
|
|
139
|
+
if (!manifestPath || !fs.existsSync(manifestPath)) {
|
|
140
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
141
|
+
}
|
|
142
|
+
if (!output) {
|
|
143
|
+
throw new Error("--output <path.png> is required");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
147
|
+
const viewport = manifest.viewport || {
|
|
148
|
+
width: 1440,
|
|
149
|
+
height: 900,
|
|
150
|
+
deviceScaleFactor: 2,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
console.log(chalk.cyan("šØ Variation render"));
|
|
154
|
+
console.log(chalk.gray(` manifest: ${manifestPath}`));
|
|
155
|
+
|
|
156
|
+
const sourceRef = await resolveSourceMhtml({
|
|
157
|
+
source,
|
|
158
|
+
scenarioKey: scenario,
|
|
159
|
+
captureKey: capture,
|
|
160
|
+
theme,
|
|
161
|
+
});
|
|
162
|
+
if (!sourceRef) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"Could not resolve source MHTML. Provide --source <path> or --scenario <key> --capture <key>.",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
console.log(chalk.gray(` source: ${sourceRef.path}`));
|
|
168
|
+
|
|
169
|
+
const browser = await chromium.launch({ headless });
|
|
170
|
+
const ctx = await browser.newContext({
|
|
171
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
172
|
+
deviceScaleFactor: viewport.deviceScaleFactor || 2,
|
|
173
|
+
colorScheme: manifest.colorScheme === "dark" ? "dark" : "light",
|
|
174
|
+
javaScriptEnabled: true,
|
|
175
|
+
});
|
|
176
|
+
const page = await ctx.newPage();
|
|
177
|
+
|
|
178
|
+
const fileUrl = `file://${path.resolve(sourceRef.path)}`;
|
|
179
|
+
await page.goto(fileUrl, { waitUntil: "domcontentloaded" });
|
|
180
|
+
await page.waitForLoadState("load");
|
|
181
|
+
await page.waitForTimeout(400);
|
|
182
|
+
|
|
183
|
+
if (manifest.mutations?.length) {
|
|
184
|
+
await page.evaluate(applyMutationsInBrowser, JSON.stringify(manifest));
|
|
185
|
+
await page.waitForTimeout(200);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fs.ensureDirSync(path.dirname(output));
|
|
189
|
+
await page.screenshot({ path: output });
|
|
190
|
+
console.log(chalk.green(`ā Rendered: ${output}`));
|
|
191
|
+
await browser.close();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = variationCommand;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runVerifyPublish } = require("../lib/certification");
|
|
5
|
+
|
|
6
|
+
async function verifyPublishCommand(options = {}) {
|
|
7
|
+
const scenarioKeys = options.scenarios
|
|
8
|
+
? String(options.scenarios)
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((value) => value.trim())
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
: null;
|
|
13
|
+
|
|
14
|
+
const report = await runVerifyPublish({
|
|
15
|
+
scenarioKeys,
|
|
16
|
+
tag: options.tag,
|
|
17
|
+
message: options.message,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log(JSON.stringify(report, null, 2));
|
|
22
|
+
} else {
|
|
23
|
+
console.log(chalk.cyan("\nš¦ Publish Verification\n"));
|
|
24
|
+
console.log(
|
|
25
|
+
report.ok
|
|
26
|
+
? chalk.green(" ā Publish, pull, and hosted delivery verified")
|
|
27
|
+
: chalk.red(" ā Publish verification failed"),
|
|
28
|
+
);
|
|
29
|
+
console.log(chalk.gray(` Published: ${report.publishResult.assetsProcessed || 0}`));
|
|
30
|
+
console.log(chalk.gray(` Pull repairs: ${report.pullResult.normalizationRepairs || 0}`));
|
|
31
|
+
const failedChecks = (report.deliveryChecks || []).filter((check) => !check.ok);
|
|
32
|
+
if (failedChecks.length > 0) {
|
|
33
|
+
for (const check of failedChecks.slice(0, 10)) {
|
|
34
|
+
console.log(chalk.red(` ā ${check.scenario}/${check.assetKey}: ${check.reason}`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!report.ok) {
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return report;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = verifyPublishCommand;
|