@reshotdev/screenshot 0.0.1-beta.2 → 0.0.1-beta.20
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/lib/config.js
CHANGED
|
@@ -21,6 +21,11 @@ const {
|
|
|
21
21
|
getConfigDefaults,
|
|
22
22
|
validateCaptureRequirements,
|
|
23
23
|
} = require("./standalone-mode");
|
|
24
|
+
const {
|
|
25
|
+
normalizeConfigContract,
|
|
26
|
+
validateNormalizedConfig,
|
|
27
|
+
getCertifiedScenarioKeys,
|
|
28
|
+
} = require("./target-contract");
|
|
24
29
|
|
|
25
30
|
const SETTINGS_DIR = ".reshot";
|
|
26
31
|
const SETTINGS_PATH = path.join(process.cwd(), SETTINGS_DIR, "settings.json");
|
|
@@ -64,7 +69,7 @@ function createAuthErrorResponse(message) {
|
|
|
64
69
|
code: "AUTH_REQUIRED",
|
|
65
70
|
};
|
|
66
71
|
}
|
|
67
|
-
const CONFIG_PATH = path.join(process.cwd(), "
|
|
72
|
+
const CONFIG_PATH = path.join(process.cwd(), "reshot.config.json");
|
|
68
73
|
const WORKSPACE_PATH = path.join(process.cwd(), SETTINGS_DIR, "workspace.json");
|
|
69
74
|
|
|
70
75
|
/**
|
|
@@ -279,13 +284,19 @@ function readConfig() {
|
|
|
279
284
|
);
|
|
280
285
|
}
|
|
281
286
|
|
|
282
|
-
const
|
|
287
|
+
const rawConfig = fs.readJSONSync(CONFIG_PATH);
|
|
288
|
+
const config = normalizeConfigContract(rawConfig);
|
|
283
289
|
|
|
284
290
|
// Validate required fields
|
|
285
291
|
if (!config.scenarios || !Array.isArray(config.scenarios)) {
|
|
286
292
|
throw new Error('Config must have a "scenarios" array');
|
|
287
293
|
}
|
|
288
294
|
|
|
295
|
+
const targetValidation = validateNormalizedConfig(config);
|
|
296
|
+
if (!targetValidation.valid) {
|
|
297
|
+
throw new Error(targetValidation.errors.join("\n"));
|
|
298
|
+
}
|
|
299
|
+
|
|
289
300
|
const validActions = [
|
|
290
301
|
"click",
|
|
291
302
|
"type",
|
|
@@ -319,9 +330,9 @@ function readConfig() {
|
|
|
319
330
|
if (!scenario.key) {
|
|
320
331
|
throw new Error(`Scenario "${scenario.name}" must have a "key" field`);
|
|
321
332
|
}
|
|
322
|
-
if (!/^[a-z0-9-]
|
|
333
|
+
if (!/^[a-z0-9]([a-z0-9\-_/]*[a-z0-9])?$/i.test(scenario.key)) {
|
|
323
334
|
throw new Error(
|
|
324
|
-
`Scenario "${scenario.name}" has invalid key "${scenario.key}". Keys must
|
|
335
|
+
`Scenario "${scenario.name}" has invalid key "${scenario.key}". Keys must start and end alphanumeric; hyphens, underscores, and slashes are allowed in between.`
|
|
325
336
|
);
|
|
326
337
|
}
|
|
327
338
|
if (!scenario.url) {
|
|
@@ -470,84 +481,22 @@ function readConfig() {
|
|
|
470
481
|
}
|
|
471
482
|
}
|
|
472
483
|
|
|
473
|
-
// Validate optional docs block
|
|
474
|
-
if (config.docs !== undefined) {
|
|
475
|
-
if (
|
|
476
|
-
typeof config.docs !== "object" ||
|
|
477
|
-
config.docs === null ||
|
|
478
|
-
Array.isArray(config.docs)
|
|
479
|
-
) {
|
|
480
|
-
throw new Error("docs must be an object");
|
|
481
|
-
}
|
|
482
|
-
if (
|
|
483
|
-
config.docs.root !== undefined &&
|
|
484
|
-
typeof config.docs.root !== "string"
|
|
485
|
-
) {
|
|
486
|
-
throw new Error("docs.root must be a string");
|
|
487
|
-
}
|
|
488
|
-
if (
|
|
489
|
-
config.docs.include !== undefined &&
|
|
490
|
-
!Array.isArray(config.docs.include)
|
|
491
|
-
) {
|
|
492
|
-
throw new Error("docs.include must be an array of strings");
|
|
493
|
-
}
|
|
494
|
-
if (
|
|
495
|
-
config.docs.exclude !== undefined &&
|
|
496
|
-
!Array.isArray(config.docs.exclude)
|
|
497
|
-
) {
|
|
498
|
-
throw new Error("docs.exclude must be an array of strings");
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
484
|
return config;
|
|
503
485
|
}
|
|
504
486
|
|
|
505
487
|
/**
|
|
506
|
-
* Read
|
|
507
|
-
*
|
|
508
|
-
* @returns {Object}
|
|
488
|
+
* Read reshot.config.json without requiring scenarios array
|
|
489
|
+
* Used by sync and other commands that don't need scenario validation
|
|
490
|
+
* @returns {Object} Configuration
|
|
509
491
|
*/
|
|
510
|
-
function
|
|
492
|
+
function readConfigLenient() {
|
|
511
493
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
512
494
|
throw new Error(
|
|
513
495
|
`Config file not found at ${CONFIG_PATH}. Run \`reshot init\` to create one.`
|
|
514
496
|
);
|
|
515
497
|
}
|
|
516
498
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
// Validate documentation block if present
|
|
520
|
-
if (config.documentation) {
|
|
521
|
-
const doc = config.documentation;
|
|
522
|
-
|
|
523
|
-
// Validate required fields
|
|
524
|
-
if (!doc.strategy) {
|
|
525
|
-
throw new Error('documentation.strategy is required (git_pr or external_host)');
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (!['git_pr', 'external_host'].includes(doc.strategy)) {
|
|
529
|
-
throw new Error('documentation.strategy must be "git_pr" or "external_host"');
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Validate optional fields
|
|
533
|
-
if (doc.assetFormat && !['cdn_link', 'markdown'].includes(doc.assetFormat)) {
|
|
534
|
-
throw new Error('documentation.assetFormat must be "cdn_link" or "markdown"');
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (doc.include && !Array.isArray(doc.include)) {
|
|
538
|
-
throw new Error('documentation.include must be an array of glob patterns');
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (doc.exclude && !Array.isArray(doc.exclude)) {
|
|
542
|
-
throw new Error('documentation.exclude must be an array of glob patterns');
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (doc.mappings && typeof doc.mappings !== 'object') {
|
|
546
|
-
throw new Error('documentation.mappings must be an object');
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return config;
|
|
499
|
+
return normalizeConfigContract(fs.readJSONSync(CONFIG_PATH));
|
|
551
500
|
}
|
|
552
501
|
|
|
553
502
|
/**
|
|
@@ -555,7 +504,7 @@ function readDocSyncConfig() {
|
|
|
555
504
|
* @param {Object} config - Config object to write
|
|
556
505
|
*/
|
|
557
506
|
function writeConfig(config) {
|
|
558
|
-
fs.writeJSONSync(CONFIG_PATH, config, { spaces: 2 });
|
|
507
|
+
fs.writeJSONSync(CONFIG_PATH, normalizeConfigContract(config), { spaces: 2 });
|
|
559
508
|
}
|
|
560
509
|
|
|
561
510
|
/**
|
|
@@ -701,10 +650,10 @@ function getPrivacyConfig(scenarioOverrides = {}) {
|
|
|
701
650
|
* Default style configuration
|
|
702
651
|
*/
|
|
703
652
|
const DEFAULT_STYLE_CONFIG = {
|
|
704
|
-
enabled:
|
|
653
|
+
enabled: false,
|
|
705
654
|
frame: "none",
|
|
706
|
-
shadow: "
|
|
707
|
-
padding:
|
|
655
|
+
shadow: "none",
|
|
656
|
+
padding: 0,
|
|
708
657
|
background: "transparent",
|
|
709
658
|
borderRadius: 0,
|
|
710
659
|
};
|
|
@@ -851,8 +800,6 @@ async function initializeProject(projectId, apiKey, options = {}) {
|
|
|
851
800
|
contextCount: 1,
|
|
852
801
|
features: {
|
|
853
802
|
visuals: true,
|
|
854
|
-
docs: false,
|
|
855
|
-
changelog: true,
|
|
856
803
|
},
|
|
857
804
|
},
|
|
858
805
|
};
|
|
@@ -1040,19 +987,175 @@ function getModeInfo() {
|
|
|
1040
987
|
};
|
|
1041
988
|
}
|
|
1042
989
|
|
|
990
|
+
function readSettingsSafe() {
|
|
991
|
+
try {
|
|
992
|
+
return readSettings();
|
|
993
|
+
} catch (error) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function collectTemplateVariables(value) {
|
|
999
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const variables = new Set();
|
|
1004
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
1005
|
+
let match = regex.exec(value);
|
|
1006
|
+
|
|
1007
|
+
while (match) {
|
|
1008
|
+
variables.add(match[1]);
|
|
1009
|
+
match = regex.exec(value);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return [...variables];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function hasDeterministicReadyContract(scenario) {
|
|
1016
|
+
if (!scenario || typeof scenario !== "object") {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (scenario.readySelector) {
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (scenario.ready?.selector || scenario.ready?.expression) {
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return Array.isArray(scenario.steps)
|
|
1029
|
+
? scenario.steps.some(
|
|
1030
|
+
(step) => step?.action === "waitForSelector" && step?.selector,
|
|
1031
|
+
)
|
|
1032
|
+
: false;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1043
1035
|
/**
|
|
1044
1036
|
* Validate full configuration for capture readiness
|
|
1037
|
+
* @param {Object} [options] - Validation options
|
|
1038
|
+
* @param {string[]} [options.scenarioKeys] - Restrict validation to scenario keys
|
|
1039
|
+
* @param {boolean} [options.requireReadyContract] - Enforce deterministic readiness
|
|
1045
1040
|
* @returns {Object} Validation result
|
|
1046
1041
|
*/
|
|
1047
|
-
function validateConfig() {
|
|
1042
|
+
function validateConfig(options = {}) {
|
|
1043
|
+
const { scenarioKeys = null, requireReadyContract = false } = options;
|
|
1044
|
+
|
|
1048
1045
|
try {
|
|
1049
1046
|
const config = readConfig();
|
|
1050
|
-
|
|
1047
|
+
const baseResult = validateCaptureRequirements(config);
|
|
1048
|
+
const errors = [...baseResult.errors];
|
|
1049
|
+
const warnings = baseResult.warnings.filter(
|
|
1050
|
+
(warning) => warning !== "baseUrl should start with http:// or https://",
|
|
1051
|
+
);
|
|
1052
|
+
const scenarios = Array.isArray(config.scenarios) ? config.scenarios : [];
|
|
1053
|
+
const requestedScenarioKeys = Array.isArray(scenarioKeys)
|
|
1054
|
+
? scenarioKeys.filter(Boolean)
|
|
1055
|
+
: [];
|
|
1056
|
+
const selectedScenarios = requestedScenarioKeys.length
|
|
1057
|
+
? scenarios.filter((scenario) => requestedScenarioKeys.includes(scenario.key))
|
|
1058
|
+
: scenarios;
|
|
1059
|
+
const missingScenarioKeys = requestedScenarioKeys.filter(
|
|
1060
|
+
(key) => !scenarios.some((scenario) => scenario.key === key),
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
if (missingScenarioKeys.length > 0) {
|
|
1064
|
+
errors.push(
|
|
1065
|
+
`Unknown scenario key(s): ${missingScenarioKeys.join(", ")}. ` +
|
|
1066
|
+
`Available scenarios: ${scenarios.map((scenario) => scenario.key).join(", ") || "none"}`,
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (config.baseUrl) {
|
|
1071
|
+
try {
|
|
1072
|
+
const parsedUrl = new URL(config.baseUrl);
|
|
1073
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
1074
|
+
errors.push(
|
|
1075
|
+
`baseUrl must use http:// or https:// (received ${parsedUrl.protocol})`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
errors.push(
|
|
1080
|
+
`baseUrl must be a valid absolute URL. Received: ${config.baseUrl}`,
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const settings = readSettingsSafe();
|
|
1086
|
+
const configuredVariables = {
|
|
1087
|
+
...(settings?.urlVariables || {}),
|
|
1088
|
+
};
|
|
1089
|
+
if (!configuredVariables.PROJECT_ID && settings?.projectId) {
|
|
1090
|
+
configuredVariables.PROJECT_ID = settings.projectId;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const unresolvedVariables = [];
|
|
1094
|
+
for (const scenario of selectedScenarios) {
|
|
1095
|
+
for (const variableName of collectTemplateVariables(scenario.url)) {
|
|
1096
|
+
if (!configuredVariables[variableName] && !process.env[variableName]) {
|
|
1097
|
+
unresolvedVariables.push(`${scenario.key}:${variableName}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (unresolvedVariables.length > 0) {
|
|
1103
|
+
errors.push(
|
|
1104
|
+
`Unresolved URL variable(s): ${unresolvedVariables.join(", ")}. ` +
|
|
1105
|
+
`Set them in .reshot/settings.json under urlVariables or as environment variables.`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const shouldRequireReadyContract =
|
|
1110
|
+
requireReadyContract || config.target?.tier === "certified";
|
|
1111
|
+
if (shouldRequireReadyContract) {
|
|
1112
|
+
const missingReadyContract = selectedScenarios
|
|
1113
|
+
.filter((scenario) => !hasDeterministicReadyContract(scenario))
|
|
1114
|
+
.map((scenario) => scenario.key);
|
|
1115
|
+
|
|
1116
|
+
if (missingReadyContract.length > 0) {
|
|
1117
|
+
errors.push(
|
|
1118
|
+
`Deterministic readiness is required for: ${missingReadyContract.join(", ")}. ` +
|
|
1119
|
+
`Add ready.selector, ready.expression, readySelector, or a waitForSelector step.`,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const authScenarioCount = selectedScenarios.filter(
|
|
1125
|
+
(scenario) => scenario.captureClass === "live-auth" || scenario.requiresAuth,
|
|
1126
|
+
).length;
|
|
1127
|
+
const liveAuthScenarioCount = selectedScenarios.filter(
|
|
1128
|
+
(scenario) => scenario.captureClass === "live-auth",
|
|
1129
|
+
).length;
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
valid: errors.length === 0,
|
|
1133
|
+
errors,
|
|
1134
|
+
warnings,
|
|
1135
|
+
details: {
|
|
1136
|
+
baseUrl: config.baseUrl || null,
|
|
1137
|
+
selectedScenarioKeys: selectedScenarios.map((scenario) => scenario.key),
|
|
1138
|
+
missingScenarioKeys,
|
|
1139
|
+
unresolvedVariables,
|
|
1140
|
+
requiresReadyContract: shouldRequireReadyContract,
|
|
1141
|
+
authScenarioCount,
|
|
1142
|
+
liveAuthScenarioCount,
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1051
1145
|
} catch (e) {
|
|
1052
1146
|
return {
|
|
1053
1147
|
valid: false,
|
|
1054
1148
|
errors: [e.message],
|
|
1055
1149
|
warnings: [],
|
|
1150
|
+
details: {
|
|
1151
|
+
baseUrl: null,
|
|
1152
|
+
selectedScenarioKeys: [],
|
|
1153
|
+
missingScenarioKeys: [],
|
|
1154
|
+
unresolvedVariables: [],
|
|
1155
|
+
requiresReadyContract: requireReadyContract,
|
|
1156
|
+
authScenarioCount: 0,
|
|
1157
|
+
liveAuthScenarioCount: 0,
|
|
1158
|
+
},
|
|
1056
1159
|
};
|
|
1057
1160
|
}
|
|
1058
1161
|
}
|
|
@@ -1230,8 +1333,10 @@ module.exports = {
|
|
|
1230
1333
|
// Mode & validation
|
|
1231
1334
|
getModeInfo,
|
|
1232
1335
|
validateConfig,
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1336
|
+
// Lenient config read (no scenario validation)
|
|
1337
|
+
readConfigLenient,
|
|
1338
|
+
// Certified target contract helpers
|
|
1339
|
+
getCertifiedScenarioKeys,
|
|
1235
1340
|
// Paths
|
|
1236
1341
|
SETTINGS_PATH,
|
|
1237
1342
|
SETTINGS_DIR,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// dom-capture.js — CLI integration layer for the Tier-3 DOM-capture engine.
|
|
2
|
+
//
|
|
3
|
+
// The capture techniques (m7 hybrid primary, m4 CDP DOMSnapshot fallback) live in
|
|
4
|
+
// @reshot/compose/capture so they can reuse the deterministic renderer + the
|
|
5
|
+
// calibrated verify evaluator and be gated under `pnpm --dir packages/compose
|
|
6
|
+
// test`. This module is the thin CLI-side wrapper: it loads that engine (with a
|
|
7
|
+
// monorepo dist fallback, mirroring commands/compose.js) and exposes the two
|
|
8
|
+
// entry points the CLI needs.
|
|
9
|
+
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const fs = require("fs-extra");
|
|
12
|
+
const { composeDistDir } = require("./compose-runtime");
|
|
13
|
+
|
|
14
|
+
function loadCaptureEngine() {
|
|
15
|
+
return require(path.join(composeDistDir(), "capture.cjs"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Capture a DOM artifact from an ALREADY-NAVIGATED live page and write it next to
|
|
20
|
+
* the other capture outputs. Returns the metadata.domArtifact block (or null if
|
|
21
|
+
* capture failed — capture is additive and must never break the video path).
|
|
22
|
+
*
|
|
23
|
+
* @param {object} args
|
|
24
|
+
* @param {import('playwright').Page} args.page live page (capture as-is)
|
|
25
|
+
* @param {string} args.outputDir
|
|
26
|
+
* @param {string} args.slug
|
|
27
|
+
* @param {string|null} [args.csp]
|
|
28
|
+
*/
|
|
29
|
+
async function captureDomArtifact({ page, outputDir, slug, csp = null }) {
|
|
30
|
+
try {
|
|
31
|
+
const { captureDom, writeArtifact } = loadCaptureEngine();
|
|
32
|
+
const snapshot = await captureDom(page, { csp });
|
|
33
|
+
const base = path.join(outputDir, slug);
|
|
34
|
+
const paths = await writeArtifact(snapshot, base);
|
|
35
|
+
return { path: paths.html, method: snapshot.method, sidecars: snapshot.surfaces };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
console.warn(` ⚠ DOM capture skipped: ${message}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Standalone capture of a URL: navigates, captures, remounts, and writes the
|
|
45
|
+
* artifact + remounted.png + live.png into outDir. Returns a result summary.
|
|
46
|
+
*/
|
|
47
|
+
async function captureDomFromUrl({ url, outDir, slug = "capture", settings }) {
|
|
48
|
+
const { captureUrl, writeArtifact } = loadCaptureEngine();
|
|
49
|
+
await fs.ensureDir(outDir);
|
|
50
|
+
const result = await captureUrl(url, settings ? { settings } : {});
|
|
51
|
+
const paths = await writeArtifact(result.snapshot, path.join(outDir, slug));
|
|
52
|
+
await fs.writeFile(path.join(outDir, "remounted.png"), result.remountPng);
|
|
53
|
+
await fs.writeFile(path.join(outDir, "live.png"), result.livePng);
|
|
54
|
+
return {
|
|
55
|
+
method: result.method,
|
|
56
|
+
artifact: paths.html,
|
|
57
|
+
meta: paths.meta,
|
|
58
|
+
remounted: path.join(outDir, "remounted.png"),
|
|
59
|
+
live: path.join(outDir, "live.png"),
|
|
60
|
+
sidecars: result.snapshot.surfaces,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { loadCaptureEngine, captureDomArtifact, captureDomFromUrl };
|
|
@@ -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
|
+
};
|
|
@@ -54,8 +54,8 @@ const TEMPLATE_PRESETS = {
|
|
|
54
54
|
// GitHub Pages / Static site friendly
|
|
55
55
|
"static-site": "./public/screenshots/{{locale}}/{{scenario}}/{{name}}.{{ext}}",
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
|
|
57
|
+
// Local CLI artifacts
|
|
58
|
+
cli: "./artifacts/screenshots/{{scenario}}/{{viewport}}/{{variant}}/{{name}}.{{ext}}",
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -291,7 +291,7 @@ function getTemplatePresets() {
|
|
|
291
291
|
{ name: "docs", template: TEMPLATE_PRESETS.docs, description: "Simple docs asset structure" },
|
|
292
292
|
{ name: "variant-matrix", template: TEMPLATE_PRESETS["variant-matrix"], description: "Hierarchical by variant dimensions" },
|
|
293
293
|
{ name: "static-site", template: TEMPLATE_PRESETS["static-site"], description: "GitHub Pages / static site friendly" },
|
|
294
|
-
{ name: "
|
|
294
|
+
{ name: "cli", template: TEMPLATE_PRESETS.cli, description: "Local CLI artifact organization" },
|
|
295
295
|
];
|
|
296
296
|
}
|
|
297
297
|
|