pagetrace 0.4.0 → 0.5.0
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/CHANGELOG.md +25 -0
- package/README.md +23 -2
- package/dist/cli.cjs +30 -12
- package/dist/cli.js +30 -12
- package/dist/index.cjs +27 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +26 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
While the version is below 1.0.0, breaking changes ship in a minor release.
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `check --baseline-branch <ref>` reads the baseline lockfile out of a git ref
|
|
14
|
+
instead of the working tree, so a pull request can diff against `main` without
|
|
15
|
+
carrying a lockfile of its own. Commit the lockfile on the default branch only
|
|
16
|
+
and feature branches stop churning it. An unresolvable ref throws rather than
|
|
17
|
+
reading as an empty baseline, since a typo must not mean "nothing changed"; a
|
|
18
|
+
ref that simply has no lockfile yet returns nothing, which is an ordinary
|
|
19
|
+
first run.
|
|
20
|
+
- A composite GitHub Action (`action.yml`). Three lines in a workflow run the
|
|
21
|
+
check and post the findings as a pull request comment, editing the previous
|
|
22
|
+
comment on each push rather than stacking new ones. It fetches the baseline ref
|
|
23
|
+
first, since a shallow CI checkout usually has only the PR head.
|
|
24
|
+
- `snapshotFromGitRef(ref, path)` is exported.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `formatMarkdown` escapes angle brackets. `The <h1> was removed.` rendered as
|
|
29
|
+
`The was removed.` on GitHub, which parses a tag name in a table cell as
|
|
30
|
+
inline HTML and drops it — losing the part of the message that mattered, in
|
|
31
|
+
the reporter whose whole purpose is the pull request comment.
|
|
32
|
+
|
|
9
33
|
## [0.4.0] - 2026-09-06
|
|
10
34
|
|
|
11
35
|
### Changed
|
|
@@ -170,6 +194,7 @@ Initial release. `snapshot`, `check` and `audit` commands; filesystem and HTTP
|
|
|
170
194
|
crawling; diff classified by transition; absolute, cross-page and hreflang audit
|
|
171
195
|
rules; pretty, JSON, markdown, GitHub and HTML reporters.
|
|
172
196
|
|
|
197
|
+
[0.5.0]: https://github.com/shyamexe/pagetrace/compare/v0.4.0...v0.5.0
|
|
173
198
|
[0.4.0]: https://github.com/shyamexe/pagetrace/compare/v0.3.0...v0.4.0
|
|
174
199
|
[0.3.0]: https://github.com/shyamexe/pagetrace/compare/v0.2.0...v0.3.0
|
|
175
200
|
[0.2.0]: https://github.com/shyamexe/pagetrace/compare/v0.1.0...v0.2.0
|
package/README.md
CHANGED
|
@@ -151,9 +151,30 @@ Every finding has a stable `code`. Set any code to `error`, `warn`, `info`, or `
|
|
|
151
151
|
|
|
152
152
|
## CI
|
|
153
153
|
|
|
154
|
+
The GitHub Action is the shortest path. It diffs the build against the baseline committed on your default branch and leaves the result as a pull request comment, updating that same comment on each push rather than stacking new ones.
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
- uses: actions/checkout@v5
|
|
158
|
+
- run: npm ci && npm run build
|
|
159
|
+
- uses: shyamexe/pagetrace@v1
|
|
160
|
+
with:
|
|
161
|
+
dir: ./out
|
|
162
|
+
baseline-branch: main
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`baseline-branch` reads the lockfile out of a git ref rather than the working tree, so feature branches never carry one and you get no lockfile churn in pull requests. Commit the lockfile on your default branch only:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npx pagetrace snapshot --dir ./out
|
|
169
|
+
git add pagetrace.lock.json
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Needs `pull-requests: write` for the comment. Set `comment: false` to skip it, or `audit: false` for a pure regression gate.
|
|
173
|
+
|
|
174
|
+
Without the Action:
|
|
175
|
+
|
|
154
176
|
```yaml
|
|
155
|
-
- run:
|
|
156
|
-
- run: npx pagetrace check --dir ./out --format github
|
|
177
|
+
- run: npx pagetrace check --dir ./out --baseline-branch origin/main --format github
|
|
157
178
|
```
|
|
158
179
|
|
|
159
180
|
`--format` accepts `pretty`, `json`, `markdown` (sized for a PR comment), and `github` (workflow annotations).
|
package/dist/cli.cjs
CHANGED
|
@@ -866,7 +866,7 @@ function formatPretty(findings) {
|
|
|
866
866
|
function formatJson(findings) {
|
|
867
867
|
return JSON.stringify({ schemaVersion: 1, summary: summarize(findings), findings }, null, 2);
|
|
868
868
|
}
|
|
869
|
-
var escapeCell = (value) => value.replace(/\|/g, "\\|");
|
|
869
|
+
var escapeCell = (value) => value.replace(/\|/g, "\\|").replace(/</g, "<").replace(/>/g, ">");
|
|
870
870
|
function formatMarkdown(findings) {
|
|
871
871
|
const s = summarize(findings);
|
|
872
872
|
if (findings.length === 0) return "### pagetrace\n\nNo SEO/AEO changes or issues found.";
|
|
@@ -1055,8 +1055,10 @@ ${cards || "<p>No issues found.</p>"}
|
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
1057
1057
|
// src/snapshot.ts
|
|
1058
|
+
var import_node_child_process = require("child_process");
|
|
1058
1059
|
var import_promises = require("fs/promises");
|
|
1059
1060
|
var import_node_path = require("path");
|
|
1061
|
+
var import_node_util = require("util");
|
|
1060
1062
|
|
|
1061
1063
|
// src/extract.ts
|
|
1062
1064
|
var import_node_html_parser = require("node-html-parser");
|
|
@@ -1278,6 +1280,28 @@ function routeFromUrl(url) {
|
|
|
1278
1280
|
return url;
|
|
1279
1281
|
}
|
|
1280
1282
|
}
|
|
1283
|
+
var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1284
|
+
async function snapshotFromGitRef(ref, path) {
|
|
1285
|
+
try {
|
|
1286
|
+
await exec("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
1287
|
+
} catch (cause) {
|
|
1288
|
+
throw new Error(
|
|
1289
|
+
`Cannot resolve git ref "${ref}". Fetch it first \u2014 a shallow CI checkout often has only the PR head.`,
|
|
1290
|
+
{ cause }
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
let stdout;
|
|
1294
|
+
try {
|
|
1295
|
+
({ stdout } = await exec("git", ["show", `${ref}:${path}`], { maxBuffer: 256 * 1024 * 1024 }));
|
|
1296
|
+
} catch {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
return JSON.parse(stdout);
|
|
1301
|
+
} catch (cause) {
|
|
1302
|
+
throw new Error(`${path} at ${ref} is not valid JSON.`, { cause });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1281
1305
|
function sameSurface(a, b) {
|
|
1282
1306
|
const strip = (s) => JSON.stringify({ ...s, createdAt: "" });
|
|
1283
1307
|
return strip(a) === strip(b);
|
|
@@ -1478,20 +1502,14 @@ cli.command("snapshot", "Record the current SEO/AEO surface to a lockfile").opti
|
|
|
1478
1502
|
written ? import_picocolors2.default.green(`Wrote ${flags.out} \u2014 ${count} page${count === 1 ? "" : "s"}.`) : import_picocolors2.default.dim(`${flags.out} is already up to date \u2014 ${count} page${count === 1 ? "" : "s"}.`)
|
|
1479
1503
|
);
|
|
1480
1504
|
});
|
|
1481
|
-
cli.command("check", "Compare the current surface against the lockfile").option("--dir <dir>", "Directory of built HTML").option("--url <origin>", "Live origin to crawl").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--lockfile <file>", "Lockfile path", { default: DEFAULT_LOCKFILE }).option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).option("--format <format>", "pretty | json | markdown | github", { default: "pretty" }).option("--fail-on <severity>", "error | warn | info", { default: "error" }).option("--audit", "Also run absolute rules, not just the diff", { default: true }).option("--update", "Write the new state to the lockfile after reporting").action(async (flags) => {
|
|
1505
|
+
cli.command("check", "Compare the current surface against the lockfile").option("--dir <dir>", "Directory of built HTML").option("--url <origin>", "Live origin to crawl").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--lockfile <file>", "Lockfile path", { default: DEFAULT_LOCKFILE }).option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).option("--format <format>", "pretty | json | markdown | github", { default: "pretty" }).option("--fail-on <severity>", "error | warn | info", { default: "error" }).option("--audit", "Also run absolute rules, not just the diff", { default: true }).option("--update", "Write the new state to the lockfile after reporting").option("--baseline-branch <ref>", "Read the baseline lockfile from a git ref instead of disk").action(async (flags) => {
|
|
1482
1506
|
const failOn = parseFailOn(flags.failOn, false);
|
|
1483
1507
|
const config = await loadConfig(flags.config);
|
|
1484
1508
|
const next = await build(flags, config);
|
|
1485
|
-
|
|
1486
|
-
try {
|
|
1487
|
-
previous = JSON.parse(await (0, import_promises2.readFile)(flags.lockfile, "utf8"));
|
|
1488
|
-
} catch {
|
|
1489
|
-
previous = null;
|
|
1490
|
-
}
|
|
1509
|
+
const previous = flags.baselineBranch ? await snapshotFromGitRef(flags.baselineBranch, flags.lockfile) : await (0, import_promises2.readFile)(flags.lockfile, "utf8").then((text2) => JSON.parse(text2)).catch(() => null);
|
|
1491
1510
|
if (!previous) {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
);
|
|
1511
|
+
const where = flags.baselineBranch ? `No ${flags.lockfile} at ${flags.baselineBranch}.` : `No lockfile at ${flags.lockfile}.`;
|
|
1512
|
+
console.error(import_picocolors2.default.yellow(`${where} Run \`pagetrace snapshot\` first to set a baseline.`));
|
|
1495
1513
|
}
|
|
1496
1514
|
const raw = [
|
|
1497
1515
|
...previous ? diffSnapshots(previous, next) : [],
|
|
@@ -1555,7 +1573,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1555
1573
|
}
|
|
1556
1574
|
});
|
|
1557
1575
|
cli.help();
|
|
1558
|
-
cli.version("0.
|
|
1576
|
+
cli.version("0.5.0");
|
|
1559
1577
|
async function main() {
|
|
1560
1578
|
try {
|
|
1561
1579
|
cli.parse(process.argv, { run: false });
|
package/dist/cli.js
CHANGED
|
@@ -843,7 +843,7 @@ function formatPretty(findings) {
|
|
|
843
843
|
function formatJson(findings) {
|
|
844
844
|
return JSON.stringify({ schemaVersion: 1, summary: summarize(findings), findings }, null, 2);
|
|
845
845
|
}
|
|
846
|
-
var escapeCell = (value) => value.replace(/\|/g, "\\|");
|
|
846
|
+
var escapeCell = (value) => value.replace(/\|/g, "\\|").replace(/</g, "<").replace(/>/g, ">");
|
|
847
847
|
function formatMarkdown(findings) {
|
|
848
848
|
const s = summarize(findings);
|
|
849
849
|
if (findings.length === 0) return "### pagetrace\n\nNo SEO/AEO changes or issues found.";
|
|
@@ -1032,8 +1032,10 @@ ${cards || "<p>No issues found.</p>"}
|
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
1034
|
// src/snapshot.ts
|
|
1035
|
+
import { execFile } from "child_process";
|
|
1035
1036
|
import { readdir, readFile } from "fs/promises";
|
|
1036
1037
|
import { join, relative, sep } from "path";
|
|
1038
|
+
import { promisify } from "util";
|
|
1037
1039
|
|
|
1038
1040
|
// src/extract.ts
|
|
1039
1041
|
import { parse } from "node-html-parser";
|
|
@@ -1255,6 +1257,28 @@ function routeFromUrl(url) {
|
|
|
1255
1257
|
return url;
|
|
1256
1258
|
}
|
|
1257
1259
|
}
|
|
1260
|
+
var exec = promisify(execFile);
|
|
1261
|
+
async function snapshotFromGitRef(ref, path) {
|
|
1262
|
+
try {
|
|
1263
|
+
await exec("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
1264
|
+
} catch (cause) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`Cannot resolve git ref "${ref}". Fetch it first \u2014 a shallow CI checkout often has only the PR head.`,
|
|
1267
|
+
{ cause }
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
let stdout;
|
|
1271
|
+
try {
|
|
1272
|
+
({ stdout } = await exec("git", ["show", `${ref}:${path}`], { maxBuffer: 256 * 1024 * 1024 }));
|
|
1273
|
+
} catch {
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
return JSON.parse(stdout);
|
|
1278
|
+
} catch (cause) {
|
|
1279
|
+
throw new Error(`${path} at ${ref} is not valid JSON.`, { cause });
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1258
1282
|
function sameSurface(a, b) {
|
|
1259
1283
|
const strip = (s) => JSON.stringify({ ...s, createdAt: "" });
|
|
1260
1284
|
return strip(a) === strip(b);
|
|
@@ -1455,20 +1479,14 @@ cli.command("snapshot", "Record the current SEO/AEO surface to a lockfile").opti
|
|
|
1455
1479
|
written ? pc2.green(`Wrote ${flags.out} \u2014 ${count} page${count === 1 ? "" : "s"}.`) : pc2.dim(`${flags.out} is already up to date \u2014 ${count} page${count === 1 ? "" : "s"}.`)
|
|
1456
1480
|
);
|
|
1457
1481
|
});
|
|
1458
|
-
cli.command("check", "Compare the current surface against the lockfile").option("--dir <dir>", "Directory of built HTML").option("--url <origin>", "Live origin to crawl").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--lockfile <file>", "Lockfile path", { default: DEFAULT_LOCKFILE }).option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).option("--format <format>", "pretty | json | markdown | github", { default: "pretty" }).option("--fail-on <severity>", "error | warn | info", { default: "error" }).option("--audit", "Also run absolute rules, not just the diff", { default: true }).option("--update", "Write the new state to the lockfile after reporting").action(async (flags) => {
|
|
1482
|
+
cli.command("check", "Compare the current surface against the lockfile").option("--dir <dir>", "Directory of built HTML").option("--url <origin>", "Live origin to crawl").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--lockfile <file>", "Lockfile path", { default: DEFAULT_LOCKFILE }).option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).option("--format <format>", "pretty | json | markdown | github", { default: "pretty" }).option("--fail-on <severity>", "error | warn | info", { default: "error" }).option("--audit", "Also run absolute rules, not just the diff", { default: true }).option("--update", "Write the new state to the lockfile after reporting").option("--baseline-branch <ref>", "Read the baseline lockfile from a git ref instead of disk").action(async (flags) => {
|
|
1459
1483
|
const failOn = parseFailOn(flags.failOn, false);
|
|
1460
1484
|
const config = await loadConfig(flags.config);
|
|
1461
1485
|
const next = await build(flags, config);
|
|
1462
|
-
|
|
1463
|
-
try {
|
|
1464
|
-
previous = JSON.parse(await readFile2(flags.lockfile, "utf8"));
|
|
1465
|
-
} catch {
|
|
1466
|
-
previous = null;
|
|
1467
|
-
}
|
|
1486
|
+
const previous = flags.baselineBranch ? await snapshotFromGitRef(flags.baselineBranch, flags.lockfile) : await readFile2(flags.lockfile, "utf8").then((text2) => JSON.parse(text2)).catch(() => null);
|
|
1468
1487
|
if (!previous) {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
);
|
|
1488
|
+
const where = flags.baselineBranch ? `No ${flags.lockfile} at ${flags.baselineBranch}.` : `No lockfile at ${flags.lockfile}.`;
|
|
1489
|
+
console.error(pc2.yellow(`${where} Run \`pagetrace snapshot\` first to set a baseline.`));
|
|
1472
1490
|
}
|
|
1473
1491
|
const raw = [
|
|
1474
1492
|
...previous ? diffSnapshots(previous, next) : [],
|
|
@@ -1532,7 +1550,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1532
1550
|
}
|
|
1533
1551
|
});
|
|
1534
1552
|
cli.help();
|
|
1535
|
-
cli.version("0.
|
|
1553
|
+
cli.version("0.5.0");
|
|
1536
1554
|
async function main() {
|
|
1537
1555
|
try {
|
|
1538
1556
|
cli.parse(process.argv, { run: false });
|
package/dist/index.cjs
CHANGED
|
@@ -62,6 +62,7 @@ __export(src_exports, {
|
|
|
62
62
|
shouldFail: () => shouldFail,
|
|
63
63
|
shouldIgnore: () => shouldIgnore,
|
|
64
64
|
snapshotFromDir: () => snapshotFromDir,
|
|
65
|
+
snapshotFromGitRef: () => snapshotFromGitRef,
|
|
65
66
|
snapshotFromOrigin: () => snapshotFromOrigin,
|
|
66
67
|
summarize: () => summarize,
|
|
67
68
|
withGuidance: () => withGuidance
|
|
@@ -1110,7 +1111,7 @@ function formatPretty(findings) {
|
|
|
1110
1111
|
function formatJson(findings) {
|
|
1111
1112
|
return JSON.stringify({ schemaVersion: 1, summary: summarize(findings), findings }, null, 2);
|
|
1112
1113
|
}
|
|
1113
|
-
var escapeCell = (value) => value.replace(/\|/g, "\\|");
|
|
1114
|
+
var escapeCell = (value) => value.replace(/\|/g, "\\|").replace(/</g, "<").replace(/>/g, ">");
|
|
1114
1115
|
function formatMarkdown(findings) {
|
|
1115
1116
|
const s = summarize(findings);
|
|
1116
1117
|
if (findings.length === 0) return "### pagetrace\n\nNo SEO/AEO changes or issues found.";
|
|
@@ -1299,8 +1300,10 @@ ${cards || "<p>No issues found.</p>"}
|
|
|
1299
1300
|
}
|
|
1300
1301
|
|
|
1301
1302
|
// src/snapshot.ts
|
|
1303
|
+
var import_node_child_process = require("child_process");
|
|
1302
1304
|
var import_promises = require("fs/promises");
|
|
1303
1305
|
var import_node_path = require("path");
|
|
1306
|
+
var import_node_util = require("util");
|
|
1304
1307
|
function routeFromFilePath(root, filePath) {
|
|
1305
1308
|
const rel = (0, import_node_path.relative)(root, filePath).split(import_node_path.sep).join("/");
|
|
1306
1309
|
const withoutExt = rel.replace(/\.html?$/i, "");
|
|
@@ -1316,6 +1319,28 @@ function routeFromUrl(url) {
|
|
|
1316
1319
|
return url;
|
|
1317
1320
|
}
|
|
1318
1321
|
}
|
|
1322
|
+
var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1323
|
+
async function snapshotFromGitRef(ref, path) {
|
|
1324
|
+
try {
|
|
1325
|
+
await exec("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
1326
|
+
} catch (cause) {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
`Cannot resolve git ref "${ref}". Fetch it first \u2014 a shallow CI checkout often has only the PR head.`,
|
|
1329
|
+
{ cause }
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
let stdout;
|
|
1333
|
+
try {
|
|
1334
|
+
({ stdout } = await exec("git", ["show", `${ref}:${path}`], { maxBuffer: 256 * 1024 * 1024 }));
|
|
1335
|
+
} catch {
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
return JSON.parse(stdout);
|
|
1340
|
+
} catch (cause) {
|
|
1341
|
+
throw new Error(`${path} at ${ref} is not valid JSON.`, { cause });
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1319
1344
|
function sameSurface(a, b) {
|
|
1320
1345
|
const strip = (s) => JSON.stringify({ ...s, createdAt: "" });
|
|
1321
1346
|
return strip(a) === strip(b);
|
|
@@ -1490,6 +1515,7 @@ async function snapshotFromOrigin(origin, options = {}) {
|
|
|
1490
1515
|
shouldFail,
|
|
1491
1516
|
shouldIgnore,
|
|
1492
1517
|
snapshotFromDir,
|
|
1518
|
+
snapshotFromGitRef,
|
|
1493
1519
|
snapshotFromOrigin,
|
|
1494
1520
|
summarize,
|
|
1495
1521
|
withGuidance
|
package/dist/index.d.cts
CHANGED
|
@@ -215,6 +215,14 @@ declare const DEFAULT_AI_AGENTS: string[];
|
|
|
215
215
|
|
|
216
216
|
declare function routeFromFilePath(root: string, filePath: string): string;
|
|
217
217
|
declare function routeFromUrl(url: string): string;
|
|
218
|
+
/**
|
|
219
|
+
* Read a committed lockfile out of a git ref rather than the working tree, so a
|
|
220
|
+
* pull request can diff against the baseline on `main` without carrying a
|
|
221
|
+
* lockfile of its own. Returns null when the ref has no lockfile at that path —
|
|
222
|
+
* an ordinary first run — but throws when the ref itself is unresolvable, since
|
|
223
|
+
* a typo in `--baseline-branch` must not read as "nothing to compare".
|
|
224
|
+
*/
|
|
225
|
+
declare function snapshotFromGitRef(ref: string, path: string): Promise<Snapshot | null>;
|
|
218
226
|
/**
|
|
219
227
|
* Whether two snapshots describe the same surface, ignoring when they were
|
|
220
228
|
* taken. The lockfile is meant to be committed, so writing a fresh timestamp on
|
|
@@ -237,4 +245,4 @@ interface CrawlOptions extends Config {
|
|
|
237
245
|
/** Build a snapshot by fetching a live origin, discovering routes via sitemap. */
|
|
238
246
|
declare function snapshotFromOrigin(origin: string, options?: CrawlOptions): Promise<Snapshot>;
|
|
239
247
|
|
|
240
|
-
export { type Aggregate, type AuditMeta, type Config, DEFAULT_AI_AGENTS, type Finding, GUIDANCE, type Guidance, type JsonLdEntity, type PageFingerprint, type Platform, RICH_RESULT_RULES, type Severity, type SiteFingerprint, type Snapshot, aggregate, applyConfig, auditCrossPage, auditHreflang, auditPage, auditSite, auditSnapshot, detectPlatform, diffPage, diffSite, diffSnapshots, extractJsonLd, extractLlmsTxt, extractPage, extractRobotsTxt, extractSitemapUrls, formatAuditHtml, formatAuditMarkdown, formatAuditPretty, formatGithub, formatJson, formatMarkdown, formatPretty, routeFromFilePath, routeFromUrl, sameSurface, shouldFail, shouldIgnore, snapshotFromDir, snapshotFromOrigin, summarize, withGuidance };
|
|
248
|
+
export { type Aggregate, type AuditMeta, type Config, DEFAULT_AI_AGENTS, type Finding, GUIDANCE, type Guidance, type JsonLdEntity, type PageFingerprint, type Platform, RICH_RESULT_RULES, type Severity, type SiteFingerprint, type Snapshot, aggregate, applyConfig, auditCrossPage, auditHreflang, auditPage, auditSite, auditSnapshot, detectPlatform, diffPage, diffSite, diffSnapshots, extractJsonLd, extractLlmsTxt, extractPage, extractRobotsTxt, extractSitemapUrls, formatAuditHtml, formatAuditMarkdown, formatAuditPretty, formatGithub, formatJson, formatMarkdown, formatPretty, routeFromFilePath, routeFromUrl, sameSurface, shouldFail, shouldIgnore, snapshotFromDir, snapshotFromGitRef, snapshotFromOrigin, summarize, withGuidance };
|
package/dist/index.d.ts
CHANGED
|
@@ -215,6 +215,14 @@ declare const DEFAULT_AI_AGENTS: string[];
|
|
|
215
215
|
|
|
216
216
|
declare function routeFromFilePath(root: string, filePath: string): string;
|
|
217
217
|
declare function routeFromUrl(url: string): string;
|
|
218
|
+
/**
|
|
219
|
+
* Read a committed lockfile out of a git ref rather than the working tree, so a
|
|
220
|
+
* pull request can diff against the baseline on `main` without carrying a
|
|
221
|
+
* lockfile of its own. Returns null when the ref has no lockfile at that path —
|
|
222
|
+
* an ordinary first run — but throws when the ref itself is unresolvable, since
|
|
223
|
+
* a typo in `--baseline-branch` must not read as "nothing to compare".
|
|
224
|
+
*/
|
|
225
|
+
declare function snapshotFromGitRef(ref: string, path: string): Promise<Snapshot | null>;
|
|
218
226
|
/**
|
|
219
227
|
* Whether two snapshots describe the same surface, ignoring when they were
|
|
220
228
|
* taken. The lockfile is meant to be committed, so writing a fresh timestamp on
|
|
@@ -237,4 +245,4 @@ interface CrawlOptions extends Config {
|
|
|
237
245
|
/** Build a snapshot by fetching a live origin, discovering routes via sitemap. */
|
|
238
246
|
declare function snapshotFromOrigin(origin: string, options?: CrawlOptions): Promise<Snapshot>;
|
|
239
247
|
|
|
240
|
-
export { type Aggregate, type AuditMeta, type Config, DEFAULT_AI_AGENTS, type Finding, GUIDANCE, type Guidance, type JsonLdEntity, type PageFingerprint, type Platform, RICH_RESULT_RULES, type Severity, type SiteFingerprint, type Snapshot, aggregate, applyConfig, auditCrossPage, auditHreflang, auditPage, auditSite, auditSnapshot, detectPlatform, diffPage, diffSite, diffSnapshots, extractJsonLd, extractLlmsTxt, extractPage, extractRobotsTxt, extractSitemapUrls, formatAuditHtml, formatAuditMarkdown, formatAuditPretty, formatGithub, formatJson, formatMarkdown, formatPretty, routeFromFilePath, routeFromUrl, sameSurface, shouldFail, shouldIgnore, snapshotFromDir, snapshotFromOrigin, summarize, withGuidance };
|
|
248
|
+
export { type Aggregate, type AuditMeta, type Config, DEFAULT_AI_AGENTS, type Finding, GUIDANCE, type Guidance, type JsonLdEntity, type PageFingerprint, type Platform, RICH_RESULT_RULES, type Severity, type SiteFingerprint, type Snapshot, aggregate, applyConfig, auditCrossPage, auditHreflang, auditPage, auditSite, auditSnapshot, detectPlatform, diffPage, diffSite, diffSnapshots, extractJsonLd, extractLlmsTxt, extractPage, extractRobotsTxt, extractSitemapUrls, formatAuditHtml, formatAuditMarkdown, formatAuditPretty, formatGithub, formatJson, formatMarkdown, formatPretty, routeFromFilePath, routeFromUrl, sameSurface, shouldFail, shouldIgnore, snapshotFromDir, snapshotFromGitRef, snapshotFromOrigin, summarize, withGuidance };
|
package/dist/index.js
CHANGED
|
@@ -1040,7 +1040,7 @@ function formatPretty(findings) {
|
|
|
1040
1040
|
function formatJson(findings) {
|
|
1041
1041
|
return JSON.stringify({ schemaVersion: 1, summary: summarize(findings), findings }, null, 2);
|
|
1042
1042
|
}
|
|
1043
|
-
var escapeCell = (value) => value.replace(/\|/g, "\\|");
|
|
1043
|
+
var escapeCell = (value) => value.replace(/\|/g, "\\|").replace(/</g, "<").replace(/>/g, ">");
|
|
1044
1044
|
function formatMarkdown(findings) {
|
|
1045
1045
|
const s = summarize(findings);
|
|
1046
1046
|
if (findings.length === 0) return "### pagetrace\n\nNo SEO/AEO changes or issues found.";
|
|
@@ -1229,8 +1229,10 @@ ${cards || "<p>No issues found.</p>"}
|
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
1231
|
// src/snapshot.ts
|
|
1232
|
+
import { execFile } from "child_process";
|
|
1232
1233
|
import { readdir, readFile } from "fs/promises";
|
|
1233
1234
|
import { join, relative, sep } from "path";
|
|
1235
|
+
import { promisify } from "util";
|
|
1234
1236
|
function routeFromFilePath(root, filePath) {
|
|
1235
1237
|
const rel = relative(root, filePath).split(sep).join("/");
|
|
1236
1238
|
const withoutExt = rel.replace(/\.html?$/i, "");
|
|
@@ -1246,6 +1248,28 @@ function routeFromUrl(url) {
|
|
|
1246
1248
|
return url;
|
|
1247
1249
|
}
|
|
1248
1250
|
}
|
|
1251
|
+
var exec = promisify(execFile);
|
|
1252
|
+
async function snapshotFromGitRef(ref, path) {
|
|
1253
|
+
try {
|
|
1254
|
+
await exec("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
1255
|
+
} catch (cause) {
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
`Cannot resolve git ref "${ref}". Fetch it first \u2014 a shallow CI checkout often has only the PR head.`,
|
|
1258
|
+
{ cause }
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
let stdout;
|
|
1262
|
+
try {
|
|
1263
|
+
({ stdout } = await exec("git", ["show", `${ref}:${path}`], { maxBuffer: 256 * 1024 * 1024 }));
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
return JSON.parse(stdout);
|
|
1269
|
+
} catch (cause) {
|
|
1270
|
+
throw new Error(`${path} at ${ref} is not valid JSON.`, { cause });
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1249
1273
|
function sameSurface(a, b) {
|
|
1250
1274
|
const strip = (s) => JSON.stringify({ ...s, createdAt: "" });
|
|
1251
1275
|
return strip(a) === strip(b);
|
|
@@ -1419,6 +1443,7 @@ export {
|
|
|
1419
1443
|
shouldFail,
|
|
1420
1444
|
shouldIgnore,
|
|
1421
1445
|
snapshotFromDir,
|
|
1446
|
+
snapshotFromGitRef,
|
|
1422
1447
|
snapshotFromOrigin,
|
|
1423
1448
|
summarize,
|
|
1424
1449
|
withGuidance
|