@reshotdev/screenshot 0.0.1-beta.13 → 0.0.1-beta.15
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/package.json +3 -3
- package/src/commands/doctor-release.js +7 -0
- package/src/commands/publish.js +79 -10
- package/src/commands/setup-wizard.js +22 -1
- package/src/index.js +3 -1
- package/src/lib/capture-engine.js +3 -2
- package/src/lib/capture-script-runner.js +10 -5
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/release-doctor.js +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reshotdev/screenshot",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.15",
|
|
4
4
|
"description": "Screenshot and video capture CLI",
|
|
5
5
|
"author": "Reshot <hello@reshot.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"playwright"
|
|
23
23
|
],
|
|
24
24
|
"bin": {
|
|
25
|
-
"reshot": "
|
|
25
|
+
"reshot": "src/index.js"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src/",
|
|
@@ -72,4 +72,4 @@
|
|
|
72
72
|
"build": "pnpm run ui:build",
|
|
73
73
|
"pack:check": "npm pack --dry-run"
|
|
74
74
|
}
|
|
75
|
-
}
|
|
75
|
+
}
|
|
@@ -52,6 +52,13 @@ async function doctorReleaseCommand(options = {}) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const advisories = report.summary?.advisories || [];
|
|
56
|
+
if (advisories.length > 0) {
|
|
57
|
+
for (const advisory of advisories.slice(0, 10)) {
|
|
58
|
+
console.log(chalk.yellow(` ⚠ ${advisory.scope}: ${advisory.message}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
if (report.reportPath) {
|
|
56
63
|
console.log(chalk.gray(`\n Report: ${report.reportPath}`));
|
|
57
64
|
}
|
package/src/commands/publish.js
CHANGED
|
@@ -595,6 +595,11 @@ function buildPublishMetadata({
|
|
|
595
595
|
}) {
|
|
596
596
|
const scenarioDefinition = buildScenarioDefinition(scenarioConfig);
|
|
597
597
|
|
|
598
|
+
// Only attach git metadata when a real commit hash is available. Sending
|
|
599
|
+
// empty git values when the repo has no HEAD causes the platform to reject
|
|
600
|
+
// the batch with an opaque 400.
|
|
601
|
+
const hasGit = !!(gitInfo && gitInfo.commitHash);
|
|
602
|
+
|
|
598
603
|
return {
|
|
599
604
|
projectId,
|
|
600
605
|
publishSessionId, // Unique ID for this CLI publish run
|
|
@@ -609,10 +614,14 @@ function buildPublishMetadata({
|
|
|
609
614
|
publish: {
|
|
610
615
|
autoApprove,
|
|
611
616
|
},
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
617
|
+
...(hasGit
|
|
618
|
+
? {
|
|
619
|
+
git: {
|
|
620
|
+
commitHash: gitInfo.commitHash,
|
|
621
|
+
commitMessage: gitInfo.commitMessage,
|
|
622
|
+
},
|
|
623
|
+
}
|
|
624
|
+
: {}),
|
|
616
625
|
cli: {
|
|
617
626
|
version: pkg.version,
|
|
618
627
|
captureTimestamp: new Date().toISOString(),
|
|
@@ -620,6 +629,24 @@ function buildPublishMetadata({
|
|
|
620
629
|
};
|
|
621
630
|
}
|
|
622
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Read pixel dimensions from a captured image so the platform can store and
|
|
634
|
+
* expose them (used by `reshot pull` for CLS-safe `<img width height>`
|
|
635
|
+
* embedding). Returns nulls for non-images or on any error — never throws.
|
|
636
|
+
*/
|
|
637
|
+
async function getImageDimensions(filePath, contentType) {
|
|
638
|
+
if (!filePath || !(contentType || "").startsWith("image/")) {
|
|
639
|
+
return { width: null, height: null };
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const sharp = require("sharp");
|
|
643
|
+
const meta = await sharp(filePath).metadata();
|
|
644
|
+
return { width: meta.width ?? null, height: meta.height ?? null };
|
|
645
|
+
} catch {
|
|
646
|
+
return { width: null, height: null };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
623
650
|
/**
|
|
624
651
|
* Publish using transactional flow (direct R2 upload with presigned URLs)
|
|
625
652
|
*/
|
|
@@ -906,12 +933,18 @@ async function publishWithTransactionalFlow(
|
|
|
906
933
|
assets: [],
|
|
907
934
|
});
|
|
908
935
|
}
|
|
936
|
+
const dimensions = await getImageDimensions(
|
|
937
|
+
result.file.path,
|
|
938
|
+
result.file.contentType,
|
|
939
|
+
);
|
|
909
940
|
groupMap.get(groupKey).assets.push({
|
|
910
941
|
key: result.file.key,
|
|
911
942
|
s3Path: result.s3Path,
|
|
912
943
|
hash: result.file.hash,
|
|
913
944
|
visualKey: result.file.visualKey,
|
|
914
945
|
size: result.file.size,
|
|
946
|
+
width: dimensions.width,
|
|
947
|
+
height: dimensions.height,
|
|
915
948
|
contentType: result.file.contentType,
|
|
916
949
|
// Include diff data from CLI analysis
|
|
917
950
|
diffPercentage: result.file.diffData?.diffPercentage ?? null,
|
|
@@ -1005,7 +1038,15 @@ async function publishWithTransactionalFlow(
|
|
|
1005
1038
|
viewUrl = batchResult.viewUrl;
|
|
1006
1039
|
}
|
|
1007
1040
|
} catch (error) {
|
|
1008
|
-
|
|
1041
|
+
// Surface the server's descriptive error body (e.g. a validation
|
|
1042
|
+
// message) instead of just axios's opaque "Request failed with status
|
|
1043
|
+
// code 400" — otherwise a 1-line fix turns into a support ticket.
|
|
1044
|
+
const serverMsg =
|
|
1045
|
+
error.response?.data?.error || error.response?.data?.message;
|
|
1046
|
+
const detail = serverMsg
|
|
1047
|
+
? `${error.message} — ${serverMsg}`
|
|
1048
|
+
: error.message;
|
|
1049
|
+
console.log(chalk.red(` ✖ Batch request failed: ${detail}`));
|
|
1009
1050
|
failCount += chunk.length;
|
|
1010
1051
|
}
|
|
1011
1052
|
}
|
|
@@ -1285,14 +1326,28 @@ function getGitInfo() {
|
|
|
1285
1326
|
try {
|
|
1286
1327
|
const commitHash = execSync("git rev-parse HEAD", {
|
|
1287
1328
|
encoding: "utf-8",
|
|
1329
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1288
1330
|
}).trim();
|
|
1289
1331
|
const commitMessage = execSync("git log -1 --pretty=%B", {
|
|
1290
1332
|
encoding: "utf-8",
|
|
1333
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1291
1334
|
}).trim();
|
|
1292
|
-
return { commitHash, commitMessage };
|
|
1335
|
+
return { commitHash, commitMessage, hasCommit: !!commitHash };
|
|
1293
1336
|
} catch (error) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1337
|
+
// No git HEAD — either not a git repo or a brand-new repo with no commits.
|
|
1338
|
+
// Proceed WITHOUT git metadata rather than sending empty values that the
|
|
1339
|
+
// platform rejects with an opaque 400 "Batch request failed".
|
|
1340
|
+
console.log(
|
|
1341
|
+
chalk.yellow(
|
|
1342
|
+
" ⚠ No git commit found — publishing without commit metadata.",
|
|
1343
|
+
),
|
|
1344
|
+
);
|
|
1345
|
+
console.log(
|
|
1346
|
+
chalk.gray(
|
|
1347
|
+
" Tip: run `git commit` first to attach commit info to this publish.",
|
|
1348
|
+
),
|
|
1349
|
+
);
|
|
1350
|
+
return { commitHash: "", commitMessage: "", hasCommit: false };
|
|
1296
1351
|
}
|
|
1297
1352
|
}
|
|
1298
1353
|
|
|
@@ -1427,9 +1482,23 @@ async function publishCommand(options = {}) {
|
|
|
1427
1482
|
};
|
|
1428
1483
|
|
|
1429
1484
|
if (!releaseDoctor.ok) {
|
|
1430
|
-
console.log(chalk.red(" ✖ Release doctor failed. Fix the
|
|
1485
|
+
console.log(chalk.red(" ✖ Release doctor failed. Fix the issues below before publishing:\n"));
|
|
1486
|
+
const blockingIssues = releaseDoctor.summary?.blockingIssues || [];
|
|
1487
|
+
if (blockingIssues.length > 0) {
|
|
1488
|
+
for (const issue of blockingIssues) {
|
|
1489
|
+
const scope = issue.scope ? `${issue.scope}: ` : "";
|
|
1490
|
+
console.log(chalk.red(` ✖ ${scope}${issue.message}`));
|
|
1491
|
+
}
|
|
1492
|
+
} else {
|
|
1493
|
+
console.log(chalk.red(" ✖ Release gate checks failed (see report for details)."));
|
|
1494
|
+
}
|
|
1495
|
+
const advisories = releaseDoctor.summary?.advisories || [];
|
|
1496
|
+
for (const advisory of advisories) {
|
|
1497
|
+
const scope = advisory.scope ? `${advisory.scope}: ` : "";
|
|
1498
|
+
console.log(chalk.yellow(` ⚠ ${scope}${advisory.message}`));
|
|
1499
|
+
}
|
|
1431
1500
|
if (releaseDoctor.reportPath) {
|
|
1432
|
-
console.log(chalk.gray(
|
|
1501
|
+
console.log(chalk.gray(`\n Full report: ${releaseDoctor.reportPath}`));
|
|
1433
1502
|
}
|
|
1434
1503
|
if (!noExit) process.exit(1);
|
|
1435
1504
|
return {
|
|
@@ -7,6 +7,16 @@ const fs = require("fs-extra");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const config = require("../lib/config");
|
|
9
9
|
const { normalizeConfigContract } = require("../lib/target-contract");
|
|
10
|
+
const { detectCI } = require("../lib/ci-detect");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether the current process can prompt the user interactively.
|
|
14
|
+
* False in CI or when stdin is not a TTY (piped / redirected), where an
|
|
15
|
+
* inquirer prompt would throw `ERR_USE_AFTER_CLOSE: readline`.
|
|
16
|
+
*/
|
|
17
|
+
function isInteractive() {
|
|
18
|
+
return !!process.stdin.isTTY && !detectCI().isCI;
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
22
|
* Detect if this is a Git repository and if it's GitHub
|
|
@@ -545,7 +555,18 @@ async function setupWizard(options = {}) {
|
|
|
545
555
|
return;
|
|
546
556
|
}
|
|
547
557
|
|
|
548
|
-
// Offer to launch studio
|
|
558
|
+
// Offer to launch studio. In non-interactive environments (CI, piped stdin)
|
|
559
|
+
// an inquirer prompt throws `ERR_USE_AFTER_CLOSE: readline`, so skip the
|
|
560
|
+
// prompt and auto-answer the safe default (do NOT launch Studio).
|
|
561
|
+
if (!isInteractive()) {
|
|
562
|
+
console.log(
|
|
563
|
+
chalk.gray(
|
|
564
|
+
"Non-interactive environment detected — skipping Studio launch. Run `reshot studio` when you want the local UI.\n",
|
|
565
|
+
),
|
|
566
|
+
);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
549
570
|
const { launchStudio } = await inquirer.prompt([
|
|
550
571
|
{
|
|
551
572
|
type: "confirm",
|
package/src/index.js
CHANGED
|
@@ -35,7 +35,9 @@ program
|
|
|
35
35
|
.action(async (options) => {
|
|
36
36
|
try {
|
|
37
37
|
const setupWizard = require("./commands/setup-wizard");
|
|
38
|
-
|
|
38
|
+
// Commander stores `--no-studio` as `options.studio === false`, not
|
|
39
|
+
// `options.noStudio`. Normalize it so the flag is honored end-to-end.
|
|
40
|
+
await setupWizard({ ...options, noStudio: options.studio === false });
|
|
39
41
|
} catch (error) {
|
|
40
42
|
console.error(chalk.red("Error:"), error.message);
|
|
41
43
|
process.exit(1);
|
|
@@ -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,
|
|
@@ -299,9 +300,9 @@ class CaptureEngine {
|
|
|
299
300
|
|
|
300
301
|
const contextOptions = this._buildContextOptions();
|
|
301
302
|
|
|
302
|
-
this.browser = await chromium
|
|
303
|
+
this.browser = await launchChromium(chromium, buildLaunchOptions({
|
|
303
304
|
headless: this.headless,
|
|
304
|
-
}));
|
|
305
|
+
}), this.logger);
|
|
305
306
|
this.context = await this.browser.newContext(contextOptions);
|
|
306
307
|
this.page = await this.context.newPage();
|
|
307
308
|
|
|
@@ -1106,8 +1106,10 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1106
1106
|
// Malformed JSON — fall through and treat as non-empty so the warning still fires.
|
|
1107
1107
|
}
|
|
1108
1108
|
}
|
|
1109
|
-
if (hasSession && !sessionIsEmpty) {
|
|
1110
|
-
// Validate session freshness with graduated warnings
|
|
1109
|
+
if (hasSession && !sessionIsEmpty && scenario.requiresAuth) {
|
|
1110
|
+
// Validate session freshness with graduated warnings.
|
|
1111
|
+
// Only relevant when this scenario actually requires auth — a leftover
|
|
1112
|
+
// session file should not trigger staleness warnings for public scenarios.
|
|
1111
1113
|
const sessionStats = fs.statSync(sessionPath);
|
|
1112
1114
|
const sessionAgeHours =
|
|
1113
1115
|
(Date.now() - sessionStats.mtimeMs) / (1000 * 60 * 60);
|
|
@@ -1938,8 +1940,10 @@ async function runScenarioWithVideoCapture(scenario, options = {}) {
|
|
|
1938
1940
|
// Check for saved session state (auth cookies) - CRITICAL for authenticated scenarios
|
|
1939
1941
|
const sessionPath = getDefaultSessionPath();
|
|
1940
1942
|
const hasSession = fs.existsSync(sessionPath);
|
|
1941
|
-
if (hasSession) {
|
|
1942
|
-
// Validate session freshness
|
|
1943
|
+
if (hasSession && scenario.requiresAuth) {
|
|
1944
|
+
// Validate session freshness. Only relevant when this scenario actually
|
|
1945
|
+
// requires auth — a leftover session file should not trigger staleness
|
|
1946
|
+
// warnings for public scenarios.
|
|
1943
1947
|
const sessionStats = fs.statSync(sessionPath);
|
|
1944
1948
|
const sessionAgeHours = (Date.now() - sessionStats.mtimeMs) / (1000 * 60 * 60);
|
|
1945
1949
|
if (sessionAgeHours > 24) {
|
|
@@ -1952,6 +1956,7 @@ async function runScenarioWithVideoCapture(scenario, options = {}) {
|
|
|
1952
1956
|
}
|
|
1953
1957
|
|
|
1954
1958
|
const { chromium } = require("playwright");
|
|
1959
|
+
const { launchChromium } = require("./ensure-browser");
|
|
1955
1960
|
// Use a unique temp directory for this recording to avoid conflicts
|
|
1956
1961
|
const recordingId = `recording-${Date.now()}-${Math.random()
|
|
1957
1962
|
.toString(36)
|
|
@@ -1978,7 +1983,7 @@ async function runScenarioWithVideoCapture(scenario, options = {}) {
|
|
|
1978
1983
|
debug("Launching browser...");
|
|
1979
1984
|
|
|
1980
1985
|
// Launch browser with video recording
|
|
1981
|
-
browser = await chromium
|
|
1986
|
+
browser = await launchChromium(chromium, buildLaunchOptions({ headless }));
|
|
1982
1987
|
debug("Browser launched successfully");
|
|
1983
1988
|
|
|
1984
1989
|
// Build context options with variant support using universal injector
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// ensure-browser.js - Guarantees the correct Playwright browser build is present
|
|
2
|
+
//
|
|
3
|
+
// The CLI bundles a specific version of Playwright, which is pinned to an exact
|
|
4
|
+
// browser build. Telling users to run a bare `npx playwright install` can resolve
|
|
5
|
+
// a DIFFERENT Playwright version (and therefore a different browser build),
|
|
6
|
+
// leaving the bundled launcher unable to find its executable. To make the build
|
|
7
|
+
// match 1:1, we drive the BUNDLED Playwright's own installer.
|
|
8
|
+
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const { spawnSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
// Matches Playwright's "missing executable" launch error.
|
|
14
|
+
const MISSING_EXECUTABLE_RE = /Executable doesn't exist|please run the following command to download new browsers|browserType\.launch.*Executable/i;
|
|
15
|
+
|
|
16
|
+
let installAttempted = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the bundled Playwright's CLI entrypoint and version.
|
|
20
|
+
* We resolve relative to the package.json so the path matches whatever
|
|
21
|
+
* Playwright build this CLI actually depends on.
|
|
22
|
+
* @returns {{ cliPath: string|null, version: string|null }}
|
|
23
|
+
*/
|
|
24
|
+
function resolveBundledPlaywright() {
|
|
25
|
+
for (const pkg of ["playwright", "playwright-core"]) {
|
|
26
|
+
try {
|
|
27
|
+
const pkgJsonPath = require.resolve(`${pkg}/package.json`);
|
|
28
|
+
const cliPath = path.join(path.dirname(pkgJsonPath), "cli.js");
|
|
29
|
+
if (fs.existsSync(cliPath)) {
|
|
30
|
+
let version = null;
|
|
31
|
+
try {
|
|
32
|
+
version = require(pkgJsonPath).version;
|
|
33
|
+
} catch (_) {
|
|
34
|
+
/* ignore */
|
|
35
|
+
}
|
|
36
|
+
return { cliPath, version, pkg };
|
|
37
|
+
}
|
|
38
|
+
} catch (_) {
|
|
39
|
+
// try next package name
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { cliPath: null, version: null, pkg: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the EXACT install command that matches the bundled Playwright build.
|
|
47
|
+
* Used both to run the install and as the fallback message shown to the user.
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function getInstallCommandString() {
|
|
51
|
+
const { cliPath } = resolveBundledPlaywright();
|
|
52
|
+
if (cliPath) {
|
|
53
|
+
return `node "${cliPath}" install chromium`;
|
|
54
|
+
}
|
|
55
|
+
return "npx playwright install chromium";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Install the chromium browser using the BUNDLED Playwright's own installer,
|
|
60
|
+
* so the browser build can never mismatch the bundled Playwright version.
|
|
61
|
+
* @param {(msg: string) => void} logger
|
|
62
|
+
* @returns {boolean} whether the install command ran successfully
|
|
63
|
+
*/
|
|
64
|
+
function installBundledChromium(logger = console.log) {
|
|
65
|
+
const { cliPath, version } = resolveBundledPlaywright();
|
|
66
|
+
if (!cliPath) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger(
|
|
71
|
+
`\n⬇️ Installing the Chromium build for Playwright${
|
|
72
|
+
version ? ` ${version}` : ""
|
|
73
|
+
} (one-time setup)...`
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Install both the headless shell and full chromium so any launch path works.
|
|
77
|
+
const result = spawnSync(
|
|
78
|
+
process.execPath,
|
|
79
|
+
[cliPath, "install", "chromium", "chromium-headless-shell"],
|
|
80
|
+
{ stdio: "inherit" }
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (result.error || result.status !== 0) {
|
|
84
|
+
logger(
|
|
85
|
+
`\n⚠ Automatic browser install failed. Please run this command manually:\n ${getInstallCommandString()}\n`
|
|
86
|
+
);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Determine whether an error is Playwright's "missing browser executable" error.
|
|
95
|
+
* @param {Error} err
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
function isMissingExecutableError(err) {
|
|
99
|
+
return !!err && MISSING_EXECUTABLE_RE.test(err.message || "");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Launch chromium, auto-installing the matching browser build on first run if
|
|
104
|
+
* the executable is missing. Retries the launch exactly once after installing.
|
|
105
|
+
*
|
|
106
|
+
* @param {import('playwright').BrowserType} chromium - the bundled chromium type
|
|
107
|
+
* @param {Object} launchOptions - options passed to chromium.launch()
|
|
108
|
+
* @param {(msg: string) => void} [logger]
|
|
109
|
+
* @returns {Promise<import('playwright').Browser>}
|
|
110
|
+
*/
|
|
111
|
+
async function launchChromium(chromium, launchOptions = {}, logger = console.log) {
|
|
112
|
+
try {
|
|
113
|
+
return await chromium.launch(launchOptions);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (!isMissingExecutableError(err)) {
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Only attempt the auto-install once per process to avoid loops.
|
|
120
|
+
if (installAttempted) {
|
|
121
|
+
const e = new Error(
|
|
122
|
+
`${err.message}\n\nThe Chromium build for this CLI is missing. Run:\n ${getInstallCommandString()}`
|
|
123
|
+
);
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
installAttempted = true;
|
|
127
|
+
|
|
128
|
+
const installed = installBundledChromium(logger);
|
|
129
|
+
if (!installed) {
|
|
130
|
+
const e = new Error(
|
|
131
|
+
`${err.message}\n\nThe Chromium build for this CLI is missing. Run:\n ${getInstallCommandString()}`
|
|
132
|
+
);
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Retry once now that the matching browser build is installed.
|
|
137
|
+
return await chromium.launch(launchOptions);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
launchChromium,
|
|
143
|
+
installBundledChromium,
|
|
144
|
+
isMissingExecutableError,
|
|
145
|
+
getInstallCommandString,
|
|
146
|
+
resolveBundledPlaywright,
|
|
147
|
+
};
|
|
@@ -275,13 +275,21 @@ async function runReleaseDoctor(options = {}) {
|
|
|
275
275
|
for (const issue of targetDoctor.summary?.advisories || []) {
|
|
276
276
|
advisories.push({ scope: "target-doctor", ...issue });
|
|
277
277
|
}
|
|
278
|
-
|
|
278
|
+
// A stale/mismatched docs asset map (e.g. src/data/reshot-assets.json left
|
|
279
|
+
// behind by an earlier `reshot pull`) describes a generated artifact, not the
|
|
280
|
+
// config being published. It must NOT hard-block a publish of the current
|
|
281
|
+
// config — surface it as an advisory with a concrete remedy instead.
|
|
282
|
+
if (!docsAssetMap.skipped && !docsAssetMap.ok) {
|
|
283
|
+
const remedy =
|
|
284
|
+
docsAssetMap.path
|
|
285
|
+
? `Re-run \`reshot pull\` to regenerate it, or delete ${docsAssetMap.path}.`
|
|
286
|
+
: "Re-run `reshot pull` to regenerate it, or delete the stale src/data/reshot-assets.json.";
|
|
279
287
|
for (const issue of docsAssetMap.issues) {
|
|
280
|
-
|
|
288
|
+
advisories.push({ scope: "docs-asset-map", message: `${issue} ${remedy}` });
|
|
281
289
|
}
|
|
282
290
|
}
|
|
283
291
|
|
|
284
|
-
const ok = preflight.ok && targetDoctor.ok
|
|
292
|
+
const ok = preflight.ok && targetDoctor.ok;
|
|
285
293
|
const report = {
|
|
286
294
|
type: "ReleaseDoctorReport",
|
|
287
295
|
stage: "doctor-release",
|