@goflag/cli 0.2.5 → 0.2.8
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/README.md +99 -14
- package/dist/{chunk-5FO2Q3II.js → chunk-SKX7UCGC.js} +318 -25
- package/dist/chunk-SKX7UCGC.js.map +1 -0
- package/dist/cli.js +1018 -217
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +64 -4
- package/dist/index.js +1 -1
- package/flags.json +302 -0
- package/package.json +7 -2
- package/rules.json +517 -0
- package/dist/chunk-5FO2Q3II.js.map +0 -1
|
@@ -970,7 +970,8 @@ async function inspect(url, options = {}) {
|
|
|
970
970
|
extractor: {
|
|
971
971
|
mode: "static",
|
|
972
972
|
escalated: false,
|
|
973
|
-
escalationReason:
|
|
973
|
+
escalationReason: heuristic.reason,
|
|
974
|
+
escalationBlocked: err.message
|
|
974
975
|
},
|
|
975
976
|
html: { static: staticBody },
|
|
976
977
|
probes: probes2
|
|
@@ -1115,6 +1116,7 @@ function push(out, seen, href, source) {
|
|
|
1115
1116
|
}
|
|
1116
1117
|
|
|
1117
1118
|
// src/lib/core/crawl.ts
|
|
1119
|
+
var MAX_ATTEMPTS = 2;
|
|
1118
1120
|
async function crawl(options) {
|
|
1119
1121
|
const depthLimit = options.depth ?? 1;
|
|
1120
1122
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
@@ -1128,6 +1130,7 @@ async function crawl(options) {
|
|
|
1128
1130
|
pages: [],
|
|
1129
1131
|
visited: [],
|
|
1130
1132
|
errors: [{ url: options.entryUrl, message: "entry URL is not a valid http(s) URL" }],
|
|
1133
|
+
recovered: [],
|
|
1131
1134
|
truncated: false
|
|
1132
1135
|
};
|
|
1133
1136
|
}
|
|
@@ -1143,6 +1146,7 @@ async function crawl(options) {
|
|
|
1143
1146
|
}
|
|
1144
1147
|
const pages = [];
|
|
1145
1148
|
const errors = [];
|
|
1149
|
+
const recovered = [];
|
|
1146
1150
|
let truncated = false;
|
|
1147
1151
|
const seenFinal = /* @__PURE__ */ new Set();
|
|
1148
1152
|
while (queue.length > 0) {
|
|
@@ -1163,9 +1167,15 @@ async function crawl(options) {
|
|
|
1163
1167
|
const results = await Promise.all(inspections);
|
|
1164
1168
|
for (const result of results) {
|
|
1165
1169
|
if (result.kind === "err") {
|
|
1170
|
+
const attempt = (result.item.attempt ?? 0) + 1;
|
|
1171
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
1172
|
+
queue.push({ ...result.item, attempt });
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1166
1175
|
errors.push({ url: result.item.url, message: result.error });
|
|
1167
1176
|
continue;
|
|
1168
1177
|
}
|
|
1178
|
+
if (result.item.attempt) recovered.push(result.item.url);
|
|
1169
1179
|
const finalUrl = canonicaliseUrl(result.page.fetch.finalUrl) ?? result.page.fetch.finalUrl;
|
|
1170
1180
|
if (seenFinal.has(finalUrl)) continue;
|
|
1171
1181
|
seenFinal.add(finalUrl);
|
|
@@ -1198,6 +1208,7 @@ async function crawl(options) {
|
|
|
1198
1208
|
pages,
|
|
1199
1209
|
visited: [...visitedSet],
|
|
1200
1210
|
errors,
|
|
1211
|
+
recovered,
|
|
1201
1212
|
truncated
|
|
1202
1213
|
};
|
|
1203
1214
|
}
|
|
@@ -1784,17 +1795,60 @@ function sortIssues(issues) {
|
|
|
1784
1795
|
});
|
|
1785
1796
|
}
|
|
1786
1797
|
|
|
1787
|
-
// src/lib/core/
|
|
1788
|
-
var
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1798
|
+
// src/lib/core/bcp47.ts
|
|
1799
|
+
var languageNames = new Intl.DisplayNames(["en"], { type: "language", fallback: "none" });
|
|
1800
|
+
var regionNames = new Intl.DisplayNames(["en"], { type: "region", fallback: "none" });
|
|
1801
|
+
var scriptNames = new Intl.DisplayNames(["en"], { type: "script", fallback: "none" });
|
|
1802
|
+
var NOT_AN_AUDIENCE_REGION = /* @__PURE__ */ new Set(["ZZ", "XA", "XB"]);
|
|
1803
|
+
var NOT_AN_AUDIENCE_LANGUAGE = /* @__PURE__ */ new Set(["mul", "zxx", "und", "mis"]);
|
|
1804
|
+
var NOT_A_SCRIPT = /* @__PURE__ */ new Set(["Zzzz"]);
|
|
1805
|
+
var ICU_KNOWS_LANGUAGES = languageNames.of("fr") !== void 0 && languageNames.of("qq") === void 0 && regionNames.of("BR") !== void 0;
|
|
1806
|
+
var ICU_UNAVAILABLE_WARNING = "This Node build cannot tell a real language tag from an invented one (small-icu). hreflang tags were checked for shape only, so `locale.invalid` may have missed tags that do not exist.";
|
|
1807
|
+
var BCP47_SHAPE = /^[a-z]{2,3}(-[a-z]{2}|-\d{3})?$/i;
|
|
1792
1808
|
function looksLikeLocaleSegment(segment) {
|
|
1793
|
-
return
|
|
1809
|
+
return BCP47_SHAPE.test(segment);
|
|
1794
1810
|
}
|
|
1795
1811
|
function isValidLocale(tag) {
|
|
1796
1812
|
if (tag === "x-default") return true;
|
|
1797
|
-
return
|
|
1813
|
+
if (!ICU_KNOWS_LANGUAGES) return BCP47_SHAPE.test(tag);
|
|
1814
|
+
let canonical;
|
|
1815
|
+
try {
|
|
1816
|
+
canonical = Intl.getCanonicalLocales(tag)[0] ?? "";
|
|
1817
|
+
} catch {
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
if (!canonical) return false;
|
|
1821
|
+
let parts;
|
|
1822
|
+
try {
|
|
1823
|
+
parts = new Intl.Locale(canonical);
|
|
1824
|
+
} catch {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
const language = (parts.language ?? "").toLowerCase();
|
|
1828
|
+
if (!language) return false;
|
|
1829
|
+
if (NOT_AN_AUDIENCE_LANGUAGE.has(language)) return false;
|
|
1830
|
+
if (languageNames.of(language) === void 0) return false;
|
|
1831
|
+
if (parts.script) {
|
|
1832
|
+
if (NOT_A_SCRIPT.has(parts.script)) return false;
|
|
1833
|
+
if (scriptNames.of(parts.script) === void 0) return false;
|
|
1834
|
+
}
|
|
1835
|
+
if (parts.region) {
|
|
1836
|
+
if (NOT_AN_AUDIENCE_REGION.has(parts.region)) return false;
|
|
1837
|
+
if (regionNames.of(parts.region) === void 0) return false;
|
|
1838
|
+
}
|
|
1839
|
+
return true;
|
|
1840
|
+
}
|
|
1841
|
+
function isKnownLanguageTag(tag) {
|
|
1842
|
+
const primary = (tag.toLowerCase().split("-")[0] ?? "").trim();
|
|
1843
|
+
if (!primary) return false;
|
|
1844
|
+
if (NOT_AN_AUDIENCE_LANGUAGE.has(primary)) return false;
|
|
1845
|
+
if (!ICU_KNOWS_LANGUAGES) return BCP47_SHAPE.test(primary);
|
|
1846
|
+
return languageNames.of(primary) !== void 0;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/lib/core/i18n.ts
|
|
1850
|
+
function localeIdentity(tag) {
|
|
1851
|
+
return tag.trim().toLowerCase();
|
|
1798
1852
|
}
|
|
1799
1853
|
function buildI18nMatrix(pages, options = {}) {
|
|
1800
1854
|
const inspectedByUrl = /* @__PURE__ */ new Map();
|
|
@@ -1802,10 +1856,18 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1802
1856
|
const grid = /* @__PURE__ */ new Map();
|
|
1803
1857
|
const locales = /* @__PURE__ */ new Set();
|
|
1804
1858
|
const routes = /* @__PURE__ */ new Set();
|
|
1859
|
+
const declaredSet = new Set(options.declaredUrls ?? []);
|
|
1805
1860
|
for (const locale of options.locales ?? []) {
|
|
1806
1861
|
const tag = locale.trim().toLowerCase();
|
|
1807
1862
|
if (tag) locales.add(tag);
|
|
1808
1863
|
}
|
|
1864
|
+
function rowOf2(url, pathnameRoute) {
|
|
1865
|
+
return options.clusterRouteOf?.(url) ?? pathnameRoute;
|
|
1866
|
+
}
|
|
1867
|
+
function sourceOf(url) {
|
|
1868
|
+
if (inspectedByUrl.has(url)) return "crawled";
|
|
1869
|
+
return declaredSet.has(url) ? "sitemap" : "alternate";
|
|
1870
|
+
}
|
|
1809
1871
|
function record(route, locale, url) {
|
|
1810
1872
|
routes.add(route);
|
|
1811
1873
|
locales.add(locale);
|
|
@@ -1815,13 +1877,13 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1815
1877
|
grid.set(route, row);
|
|
1816
1878
|
}
|
|
1817
1879
|
if (!row.has(locale)) {
|
|
1818
|
-
row.set(locale, { url, inspected: inspectedByUrl.has(url) });
|
|
1880
|
+
row.set(locale, { url, inspected: inspectedByUrl.has(url), source: sourceOf(url) });
|
|
1819
1881
|
}
|
|
1820
1882
|
}
|
|
1821
1883
|
for (const page of pages) {
|
|
1822
1884
|
const url = new URL(page.fetch.finalUrl);
|
|
1823
1885
|
const { route: selfRoute, locale: selfLocale } = splitRoute(url.pathname);
|
|
1824
|
-
record(selfRoute, selfLocale, page.fetch.finalUrl);
|
|
1886
|
+
record(rowOf2(page.fetch.finalUrl, selfRoute), selfLocale, page.fetch.finalUrl);
|
|
1825
1887
|
for (const alt of page.links.alternates) {
|
|
1826
1888
|
let altUrl;
|
|
1827
1889
|
try {
|
|
@@ -1831,7 +1893,7 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1831
1893
|
}
|
|
1832
1894
|
const { route } = splitRoute(altUrl.pathname);
|
|
1833
1895
|
const locale = alt.isXDefault ? "x-default" : localeIdentity(alt.hreflang);
|
|
1834
|
-
record(route || selfRoute, locale, altUrl.toString());
|
|
1896
|
+
record(rowOf2(altUrl.toString(), route || selfRoute), locale, altUrl.toString());
|
|
1835
1897
|
}
|
|
1836
1898
|
}
|
|
1837
1899
|
for (const declared of options.declaredUrls ?? []) {
|
|
@@ -1842,7 +1904,7 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1842
1904
|
continue;
|
|
1843
1905
|
}
|
|
1844
1906
|
const { route, locale } = splitRoute(url.pathname);
|
|
1845
|
-
record(route, locale, url.toString());
|
|
1907
|
+
record(rowOf2(url.toString(), route), locale, url.toString());
|
|
1846
1908
|
}
|
|
1847
1909
|
const sortedLocales = sortLocales([...locales]);
|
|
1848
1910
|
const sortedRoutes = [...routes].sort((a, b) => a.localeCompare(b));
|
|
@@ -1851,11 +1913,20 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1851
1913
|
cells[route] = {};
|
|
1852
1914
|
for (const locale of sortedLocales) {
|
|
1853
1915
|
const slot = grid.get(route)?.get(locale);
|
|
1854
|
-
cells[route][locale] = slot ? { url: slot.url, inspected: slot.inspected } : { url: null, inspected: false };
|
|
1916
|
+
cells[route][locale] = slot ? { url: slot.url, inspected: slot.inspected, source: slot.source } : { url: null, inspected: false };
|
|
1855
1917
|
}
|
|
1856
1918
|
}
|
|
1857
1919
|
return { locales: sortedLocales, routes: sortedRoutes, cells };
|
|
1858
1920
|
}
|
|
1921
|
+
function countUnverifiedAlternates(matrix) {
|
|
1922
|
+
let count2 = 0;
|
|
1923
|
+
for (const route of matrix.routes) {
|
|
1924
|
+
for (const locale of matrix.locales) {
|
|
1925
|
+
if (matrix.cells[route]?.[locale]?.source === "alternate") count2++;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return count2;
|
|
1929
|
+
}
|
|
1859
1930
|
function reciprocityIssues(pages) {
|
|
1860
1931
|
const issues = [];
|
|
1861
1932
|
const byUrl = /* @__PURE__ */ new Map();
|
|
@@ -1956,6 +2027,9 @@ function declaredLocales(page) {
|
|
|
1956
2027
|
}
|
|
1957
2028
|
return out;
|
|
1958
2029
|
}
|
|
2030
|
+
function rowOf(site, url, pathname) {
|
|
2031
|
+
return site.clusterRouteOf?.(url) ?? splitRoute(pathname).route;
|
|
2032
|
+
}
|
|
1959
2033
|
function sitemapLocalesByRoute(site) {
|
|
1960
2034
|
const byRoute = /* @__PURE__ */ new Map();
|
|
1961
2035
|
for (const entry of site.discovery?.urls ?? []) {
|
|
@@ -1965,8 +2039,9 @@ function sitemapLocalesByRoute(site) {
|
|
|
1965
2039
|
} catch {
|
|
1966
2040
|
continue;
|
|
1967
2041
|
}
|
|
1968
|
-
const {
|
|
2042
|
+
const { locale } = splitRoute(pathname);
|
|
1969
2043
|
if (locale === "x-default") continue;
|
|
2044
|
+
const route = rowOf(site, entry.loc, pathname);
|
|
1970
2045
|
const set = byRoute.get(route) ?? /* @__PURE__ */ new Set();
|
|
1971
2046
|
set.add(locale.toLowerCase());
|
|
1972
2047
|
byRoute.set(route, set);
|
|
@@ -2030,7 +2105,7 @@ var hreflangSitemapMismatch = {
|
|
|
2030
2105
|
} catch {
|
|
2031
2106
|
continue;
|
|
2032
2107
|
}
|
|
2033
|
-
const
|
|
2108
|
+
const route = rowOf(site, page.fetch.finalUrl, pathname);
|
|
2034
2109
|
const inSitemap = bySitemap.get(route);
|
|
2035
2110
|
if (!inSitemap || inSitemap.size === 0) continue;
|
|
2036
2111
|
const onlyInSitemap = sorted(new Set([...inSitemap].filter((l) => !head.has(l))));
|
|
@@ -2159,13 +2234,6 @@ function sortSiteIssues(drafts) {
|
|
|
2159
2234
|
}
|
|
2160
2235
|
|
|
2161
2236
|
// src/lib/core/locales.ts
|
|
2162
|
-
var ISO_639_1 = new Set(
|
|
2163
|
-
"aa ab ae af ak am an ar as av ay az ba be bg bh bi bm bn bo br bs ca ce ch co cr cs cu cv cy da de dv dz ee el en eo es et eu fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl km kn ko kr ks ku kv kw ky la lb lg li ln lo lt lu lv mg mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc oj om or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za zh zu".split(" ")
|
|
2164
|
-
);
|
|
2165
|
-
function isKnownLanguageTag(tag) {
|
|
2166
|
-
const primary = tag.toLowerCase().split("-")[0] ?? "";
|
|
2167
|
-
return ISO_639_1.has(primary);
|
|
2168
|
-
}
|
|
2169
2237
|
function localePrefixOf(url) {
|
|
2170
2238
|
let pathname;
|
|
2171
2239
|
try {
|
|
@@ -2246,6 +2314,26 @@ import { gunzipSync } from "zlib";
|
|
|
2246
2314
|
|
|
2247
2315
|
// src/lib/core/sitemap/parse.ts
|
|
2248
2316
|
import * as cheerio2 from "cheerio";
|
|
2317
|
+
function alternatesOf(node) {
|
|
2318
|
+
const out = [];
|
|
2319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2320
|
+
node.children().each((_, el) => {
|
|
2321
|
+
const tag = "tagName" in el ? el.tagName ?? "" : "";
|
|
2322
|
+
if (tag.split(":").pop()?.toLowerCase() !== "link") return;
|
|
2323
|
+
const attrs = "attribs" in el ? el.attribs : void 0;
|
|
2324
|
+
if (!attrs) return;
|
|
2325
|
+
const rel = (attrs.rel ?? "alternate").trim().toLowerCase();
|
|
2326
|
+
if (rel !== "alternate") return;
|
|
2327
|
+
const hreflang = (attrs.hreflang ?? "").trim();
|
|
2328
|
+
const href = (attrs.href ?? "").trim();
|
|
2329
|
+
if (!hreflang || !href) return;
|
|
2330
|
+
const key = `${hreflang.toLowerCase()}\0${href}`;
|
|
2331
|
+
if (seen.has(key)) return;
|
|
2332
|
+
seen.add(key);
|
|
2333
|
+
out.push({ hreflang, href });
|
|
2334
|
+
});
|
|
2335
|
+
return out;
|
|
2336
|
+
}
|
|
2249
2337
|
function parseSitemap(body) {
|
|
2250
2338
|
const trimmed = body.trim();
|
|
2251
2339
|
if (trimmed.length === 0) {
|
|
@@ -2284,6 +2372,8 @@ function parseSitemap(body) {
|
|
|
2284
2372
|
if (changefreq) entry.changefreq = changefreq;
|
|
2285
2373
|
const priority = node.find("priority").first().text().trim();
|
|
2286
2374
|
if (priority) entry.priority = priority;
|
|
2375
|
+
const alternates = alternatesOf(node);
|
|
2376
|
+
if (alternates.length > 0) entry.alternates = alternates;
|
|
2287
2377
|
urls.push(entry);
|
|
2288
2378
|
});
|
|
2289
2379
|
return { kind: "urlset", wellFormed: true, urls };
|
|
@@ -2503,6 +2593,155 @@ function relaxTlsIfRequested2(enabled) {
|
|
|
2503
2593
|
};
|
|
2504
2594
|
}
|
|
2505
2595
|
|
|
2596
|
+
// src/lib/core/clusters.ts
|
|
2597
|
+
function normalise(raw) {
|
|
2598
|
+
let url;
|
|
2599
|
+
try {
|
|
2600
|
+
url = new URL(raw);
|
|
2601
|
+
} catch {
|
|
2602
|
+
return null;
|
|
2603
|
+
}
|
|
2604
|
+
url.hash = "";
|
|
2605
|
+
const path = url.pathname.length > 1 ? url.pathname.replace(/\/+$/, "") : url.pathname;
|
|
2606
|
+
return `${url.origin}${path}${url.search}`;
|
|
2607
|
+
}
|
|
2608
|
+
var X_DEFAULT = "x-default";
|
|
2609
|
+
function clusterCount(rows) {
|
|
2610
|
+
return new Set(rows.values()).size;
|
|
2611
|
+
}
|
|
2612
|
+
function indexOf(rows, conflicts, refused) {
|
|
2613
|
+
return {
|
|
2614
|
+
rows,
|
|
2615
|
+
routeOf: (url) => {
|
|
2616
|
+
const key = normalise(url);
|
|
2617
|
+
return key === null ? void 0 : rows.get(key);
|
|
2618
|
+
},
|
|
2619
|
+
size: clusterCount(rows),
|
|
2620
|
+
conflicts,
|
|
2621
|
+
refused
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
function buildClusterIndex(entries) {
|
|
2625
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2626
|
+
const conflicts = [];
|
|
2627
|
+
let refused = 0;
|
|
2628
|
+
for (const entry of entries) {
|
|
2629
|
+
const alternates = entry.alternates;
|
|
2630
|
+
if (!alternates || alternates.length === 0) continue;
|
|
2631
|
+
const xDefault = alternates.find((a) => a.hreflang.trim().toLowerCase() === X_DEFAULT);
|
|
2632
|
+
if (!xDefault) {
|
|
2633
|
+
refused++;
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
const anchorUrl = normalise(xDefault.href);
|
|
2637
|
+
if (!anchorUrl) {
|
|
2638
|
+
refused++;
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
const { route: anchorRoute } = splitRoute(new URL(anchorUrl).pathname);
|
|
2642
|
+
const members = [entry.loc, ...alternates.map((a) => a.href)];
|
|
2643
|
+
for (const member of members) {
|
|
2644
|
+
const url = normalise(member);
|
|
2645
|
+
if (!url) continue;
|
|
2646
|
+
const existing = rows.get(url);
|
|
2647
|
+
if (existing === void 0) {
|
|
2648
|
+
rows.set(url, anchorRoute);
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
if (existing !== anchorRoute && !conflicts.includes(url)) conflicts.push(url);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
return indexOf(rows, conflicts, refused);
|
|
2655
|
+
}
|
|
2656
|
+
function readHeads(pages) {
|
|
2657
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2658
|
+
for (const page of pages) {
|
|
2659
|
+
const self = normalise(page.fetch.finalUrl);
|
|
2660
|
+
if (self === null || seen.has(self)) continue;
|
|
2661
|
+
const peers = /* @__PURE__ */ new Set();
|
|
2662
|
+
const xDefaults = /* @__PURE__ */ new Set();
|
|
2663
|
+
for (const alt of page.links.alternates) {
|
|
2664
|
+
const href = normalise(alt.href);
|
|
2665
|
+
if (href === null) continue;
|
|
2666
|
+
if (alt.isXDefault) xDefaults.add(href);
|
|
2667
|
+
else if (href !== self) peers.add(href);
|
|
2668
|
+
}
|
|
2669
|
+
seen.set(self, { peers, xDefault: xDefaults.size === 1 ? [...xDefaults][0] : null });
|
|
2670
|
+
}
|
|
2671
|
+
return seen;
|
|
2672
|
+
}
|
|
2673
|
+
function buildHeadClusterIndex(pages) {
|
|
2674
|
+
const seen = readHeads(pages);
|
|
2675
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2676
|
+
for (const url of seen.keys()) parent.set(url, url);
|
|
2677
|
+
const find = (start) => {
|
|
2678
|
+
let root = start;
|
|
2679
|
+
while (parent.get(root) !== root) root = parent.get(root);
|
|
2680
|
+
let cursor = start;
|
|
2681
|
+
while (parent.get(cursor) !== root) {
|
|
2682
|
+
const next = parent.get(cursor);
|
|
2683
|
+
parent.set(cursor, root);
|
|
2684
|
+
cursor = next;
|
|
2685
|
+
}
|
|
2686
|
+
return root;
|
|
2687
|
+
};
|
|
2688
|
+
for (const [url, decl] of seen) {
|
|
2689
|
+
for (const peer of decl.peers) {
|
|
2690
|
+
if (!seen.get(peer)?.peers.has(url)) continue;
|
|
2691
|
+
const [a, b] = [find(url), find(peer)];
|
|
2692
|
+
if (a !== b) parent.set(a, b);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
const components = /* @__PURE__ */ new Map();
|
|
2696
|
+
for (const url of seen.keys()) {
|
|
2697
|
+
const root = find(url);
|
|
2698
|
+
const members = components.get(root);
|
|
2699
|
+
if (members) members.push(url);
|
|
2700
|
+
else components.set(root, [url]);
|
|
2701
|
+
}
|
|
2702
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2703
|
+
let refused = 0;
|
|
2704
|
+
for (const members of components.values()) {
|
|
2705
|
+
if (members.length < 2) continue;
|
|
2706
|
+
const declared = new Set(
|
|
2707
|
+
members.map((m) => seen.get(m)?.xDefault).filter((x) => Boolean(x))
|
|
2708
|
+
);
|
|
2709
|
+
if (declared.size !== 1) {
|
|
2710
|
+
refused++;
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
const anchor = [...declared][0];
|
|
2714
|
+
if (!members.includes(anchor)) {
|
|
2715
|
+
refused++;
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
const { route } = splitRoute(new URL(anchor).pathname);
|
|
2719
|
+
for (const member of members) rows.set(member, route);
|
|
2720
|
+
}
|
|
2721
|
+
return indexOf(rows, [], refused);
|
|
2722
|
+
}
|
|
2723
|
+
function combineClusterIndexes(primary, secondary) {
|
|
2724
|
+
const rows = new Map(secondary.rows);
|
|
2725
|
+
const conflicts = [...primary.conflicts];
|
|
2726
|
+
const add = (url) => {
|
|
2727
|
+
if (!conflicts.includes(url)) conflicts.push(url);
|
|
2728
|
+
};
|
|
2729
|
+
for (const url of secondary.conflicts) add(url);
|
|
2730
|
+
for (const [url, route] of primary.rows) {
|
|
2731
|
+
const claimed = rows.get(url);
|
|
2732
|
+
if (claimed !== void 0 && claimed !== route) add(url);
|
|
2733
|
+
rows.set(url, route);
|
|
2734
|
+
}
|
|
2735
|
+
return indexOf(rows, conflicts, primary.refused + secondary.refused);
|
|
2736
|
+
}
|
|
2737
|
+
function clustersOnlyFrom(primary, secondary) {
|
|
2738
|
+
const routes = /* @__PURE__ */ new Set();
|
|
2739
|
+
for (const [url, route] of secondary.rows) {
|
|
2740
|
+
if (!primary.rows.has(url)) routes.add(route);
|
|
2741
|
+
}
|
|
2742
|
+
return routes.size;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2506
2745
|
// src/lib/core/coverage.ts
|
|
2507
2746
|
var DEFAULT_PER_FAMILY = 3;
|
|
2508
2747
|
var DEFAULT_THRESHOLD = 8;
|
|
@@ -3435,6 +3674,11 @@ function localeOfUrl(url) {
|
|
|
3435
3674
|
}
|
|
3436
3675
|
return null;
|
|
3437
3676
|
}
|
|
3677
|
+
function blockedEscalationWarning(pages) {
|
|
3678
|
+
const blocked = pages.filter((p) => p.extractor.escalationBlocked);
|
|
3679
|
+
if (blocked.length === 0) return null;
|
|
3680
|
+
return `${blocked.length} page(s) looked client-rendered and were judged on their static HTML, because the browser could not be started: ${blocked[0].extractor.escalationBlocked} Their metadata findings may be phantoms \u2014 install Playwright and Chromium, or pass --static to say the static HTML is what you meant to judge.`;
|
|
3681
|
+
}
|
|
3438
3682
|
function deriveTranslationHoles(matrix, ignoreRoutes = []) {
|
|
3439
3683
|
const realLocales = matrix.locales.filter((l) => l !== "x-default");
|
|
3440
3684
|
if (realLocales.length < 2) return [];
|
|
@@ -3501,6 +3745,7 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3501
3745
|
crawlFallback: false
|
|
3502
3746
|
}).catch(() => void 0);
|
|
3503
3747
|
const allSitemapUrls = (discovery?.urls ?? []).map((u) => u.loc);
|
|
3748
|
+
const sitemapClusters = buildClusterIndex(discovery?.urls ?? []);
|
|
3504
3749
|
const coverageMode = options.coverage ?? (allSitemapUrls.length > 0 ? "structural" : "all");
|
|
3505
3750
|
const selection = coverageMode === "structural" && allSitemapUrls.length > 0 ? selectByStructure(allSitemapUrls, { locales: options.locales }) : void 0;
|
|
3506
3751
|
const sitemapUrls = selection?.urls ?? allSitemapUrls;
|
|
@@ -3533,6 +3778,13 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3533
3778
|
});
|
|
3534
3779
|
const pages = crawlResult.pages;
|
|
3535
3780
|
const warnings = crawlResult.errors.map((e) => `crawl: ${e.url} \u2014 ${e.message}`);
|
|
3781
|
+
if (crawlResult.recovered.length > 0) {
|
|
3782
|
+
warnings.push(
|
|
3783
|
+
`${crawlResult.recovered.length} page(s) failed once and answered on retry: ${crawlResult.recovered.slice(0, 3).join(", ")}${crawlResult.recovered.length > 3 ? `, +${crawlResult.recovered.length - 3} more` : ""}.`
|
|
3784
|
+
);
|
|
3785
|
+
}
|
|
3786
|
+
const escalationWarning = blockedEscalationWarning(pages);
|
|
3787
|
+
if (escalationWarning) warnings.push(escalationWarning);
|
|
3536
3788
|
const okPages = pages.filter((p) => isOkStatus(p.fetch.status));
|
|
3537
3789
|
const documents = okPages.filter(isHtmlPage);
|
|
3538
3790
|
const { kept: htmlPages, dropped: duplicatePages } = dropCanonicalDuplicates(documents);
|
|
@@ -3595,10 +3847,26 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3595
3847
|
suggestion ? `No sitemap and no --locales: i18n checks are off. Locale-looking prefixes found \u2014 re-run with \`--locales ${suggestion}\` to enable them.` : `No sitemap and no --locales: i18n checks are off. Prefixes seen (${localeAxis.candidates.map((c) => c.tag).join(", ")}) do not look like locales; pass --locales if any of them is one.`
|
|
3596
3848
|
);
|
|
3597
3849
|
}
|
|
3850
|
+
if (!ICU_KNOWS_LANGUAGES) warnings.push(ICU_UNAVAILABLE_WARNING);
|
|
3851
|
+
const headClusters = buildHeadClusterIndex(htmlPages);
|
|
3852
|
+
const clusters = combineClusterIndexes(sitemapClusters, headClusters);
|
|
3853
|
+
const clustersFromHead = clustersOnlyFrom(sitemapClusters, headClusters);
|
|
3598
3854
|
const matrix = buildI18nMatrix(htmlPages, {
|
|
3599
3855
|
declaredUrls: sitemapUrls,
|
|
3600
|
-
locales: localeAxis.locales
|
|
3856
|
+
locales: localeAxis.locales,
|
|
3857
|
+
clusterRouteOf: clusters.routeOf
|
|
3601
3858
|
});
|
|
3859
|
+
if (clusters.conflicts.length > 0) {
|
|
3860
|
+
warnings.push(
|
|
3861
|
+
`${clusters.conflicts.length} URL(s) are declared in two different hreflang clusters by the sitemap and/or the pages' \`<head>\`; the sitemap's declaration was kept: ${clusters.conflicts.slice(0, 3).join(", ")}${clusters.conflicts.length > 3 ? `, +${clusters.conflicts.length - 3} more` : ""}.`
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3864
|
+
const unverifiedAlternates = countUnverifiedAlternates(matrix);
|
|
3865
|
+
if (unverifiedAlternates > 0) {
|
|
3866
|
+
warnings.push(
|
|
3867
|
+
`${unverifiedAlternates} translation${unverifiedAlternates === 1 ? "" : "s"} in the matrix ${unverifiedAlternates === 1 ? "is" : "are"} vouched for only by an \`hreflang\` on another page \u2014 not crawled, and not in the sitemap. They are counted as present, so a page advertised but not served reads as translated rather than missing.`
|
|
3868
|
+
);
|
|
3869
|
+
}
|
|
3602
3870
|
const holes = localeAxis.multilingual ? deriveTranslationHoles(matrix, options.ignoreHoles) : [];
|
|
3603
3871
|
const ignoredHoles = localeAxis.multilingual && (options.ignoreHoles?.length ?? 0) > 0 ? deriveTranslationHoles(matrix).length - holes.length : 0;
|
|
3604
3872
|
const reciprocity = reciprocityIssues(htmlPages).map((issue) => ({
|
|
@@ -3617,7 +3885,11 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3617
3885
|
matrix,
|
|
3618
3886
|
localeAxis,
|
|
3619
3887
|
discovery,
|
|
3620
|
-
robots
|
|
3888
|
+
robots,
|
|
3889
|
+
// Same index the matrix was built from, so a rule that groups by route
|
|
3890
|
+
// and the grid that reports holes cannot disagree about which URLs are
|
|
3891
|
+
// one page.
|
|
3892
|
+
clusterRouteOf: clusters.routeOf
|
|
3621
3893
|
};
|
|
3622
3894
|
const siteOccurrence = /* @__PURE__ */ new Map();
|
|
3623
3895
|
const siteIssues = lintSite(siteContext).map((draft) => {
|
|
@@ -3743,6 +4015,24 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3743
4015
|
},
|
|
3744
4016
|
...ignoredHoles > 0 ? { ignoredHoles } : {},
|
|
3745
4017
|
...duplicatePages > 0 ? { duplicatePages } : {},
|
|
4018
|
+
...unverifiedAlternates > 0 ? { unverifiedAlternates } : {},
|
|
4019
|
+
// Only when the site declared clusters. A merge that changes which rows
|
|
4020
|
+
// exist must be visible: the alternative is a hole count that moved for
|
|
4021
|
+
// a reason nothing in the report explains.
|
|
4022
|
+
...clusters.size > 0 || clusters.refused > 0 ? {
|
|
4023
|
+
declaredClusters: {
|
|
4024
|
+
count: clusters.size,
|
|
4025
|
+
...clusters.conflicts.length > 0 ? { conflicts: clusters.conflicts.length } : {},
|
|
4026
|
+
// Which source did the work. On a site that declares both ways
|
|
4027
|
+
// the two indexes describe the same clusters, so this counts the
|
|
4028
|
+
// rows only the `<head>` supplied rather than a difference of
|
|
4029
|
+
// totals that would claim a gain that is not there.
|
|
4030
|
+
...clustersFromHead > 0 ? { fromHead: clustersFromHead } : {},
|
|
4031
|
+
// Declared and not used, for want of an anchor to name the row
|
|
4032
|
+
// after. A declaration we saw and did not act on has to be said.
|
|
4033
|
+
...clusters.refused > 0 ? { refused: clusters.refused } : {}
|
|
4034
|
+
}
|
|
4035
|
+
} : {},
|
|
3746
4036
|
...discovery ? {
|
|
3747
4037
|
sitemap: {
|
|
3748
4038
|
found: discovery.diagnostics.found,
|
|
@@ -4399,9 +4689,12 @@ var Logger = class {
|
|
|
4399
4689
|
};
|
|
4400
4690
|
|
|
4401
4691
|
export {
|
|
4692
|
+
RULES,
|
|
4693
|
+
SITE_RULES,
|
|
4402
4694
|
DEFAULT_PROFILE,
|
|
4403
4695
|
PROFILES,
|
|
4404
4696
|
PROFILE_NAMES,
|
|
4697
|
+
PROSE_RULES,
|
|
4405
4698
|
routeKey,
|
|
4406
4699
|
targetKey,
|
|
4407
4700
|
fingerprint,
|
|
@@ -4414,4 +4707,4 @@ export {
|
|
|
4414
4707
|
summarize,
|
|
4415
4708
|
Logger
|
|
4416
4709
|
};
|
|
4417
|
-
//# sourceMappingURL=chunk-
|
|
4710
|
+
//# sourceMappingURL=chunk-SKX7UCGC.js.map
|