afdocs 0.1.3 → 0.3.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/README.md +48 -24
- package/dist/checks/authentication/auth-alternative-access.d.ts +2 -0
- package/dist/checks/authentication/auth-alternative-access.d.ts.map +1 -0
- package/dist/checks/authentication/auth-alternative-access.js +17 -0
- package/dist/checks/authentication/auth-alternative-access.js.map +1 -0
- package/dist/checks/authentication/auth-gate-detection.d.ts +2 -0
- package/dist/checks/authentication/auth-gate-detection.d.ts.map +1 -0
- package/dist/checks/authentication/auth-gate-detection.js +175 -0
- package/dist/checks/authentication/auth-gate-detection.js.map +1 -0
- package/dist/checks/content-structure/markdown-code-fence-validity.js +119 -5
- package/dist/checks/content-structure/markdown-code-fence-validity.js.map +1 -1
- package/dist/checks/index.d.ts +2 -0
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +3 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/llms-txt/llms-txt-exists.js +83 -6
- package/dist/checks/llms-txt/llms-txt-exists.js.map +1 -1
- package/dist/checks/llms-txt/llms-txt-links-markdown.js +46 -21
- package/dist/checks/llms-txt/llms-txt-links-markdown.js.map +1 -1
- package/dist/checks/llms-txt/llms-txt-links-resolve.js +10 -3
- package/dist/checks/llms-txt/llms-txt-links-resolve.js.map +1 -1
- package/dist/checks/markdown-availability/content-negotiation.js +101 -5
- package/dist/checks/markdown-availability/content-negotiation.js.map +1 -1
- package/dist/checks/markdown-availability/markdown-url-support.js +82 -5
- package/dist/checks/markdown-availability/markdown-url-support.js.map +1 -1
- package/dist/checks/observability/cache-header-hygiene.js +200 -5
- package/dist/checks/observability/cache-header-hygiene.js.map +1 -1
- package/dist/checks/page-size/content-start-position.js +161 -5
- package/dist/checks/page-size/content-start-position.js.map +1 -1
- package/dist/checks/page-size/page-size-html.js +127 -5
- package/dist/checks/page-size/page-size-html.js.map +1 -1
- package/dist/checks/page-size/page-size-markdown.js +100 -5
- package/dist/checks/page-size/page-size-markdown.js.map +1 -1
- package/dist/checks/url-stability/http-status-codes.js +106 -5
- package/dist/checks/url-stability/http-status-codes.js.map +1 -1
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +9 -1
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/formatters/text.d.ts +4 -1
- package/dist/cli/formatters/text.d.ts.map +1 -1
- package/dist/cli/formatters/text.js +94 -1
- package/dist/cli/formatters/text.js.map +1 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -1
- package/dist/helpers/detect-markdown.d.ts +10 -0
- package/dist/helpers/detect-markdown.d.ts.map +1 -0
- package/dist/helpers/detect-markdown.js +29 -0
- package/dist/helpers/detect-markdown.js.map +1 -0
- package/dist/helpers/get-markdown-content.d.ts +23 -0
- package/dist/helpers/get-markdown-content.d.ts.map +1 -0
- package/dist/helpers/get-markdown-content.js +93 -0
- package/dist/helpers/get-markdown-content.js.map +1 -0
- package/dist/helpers/get-page-urls.d.ts +42 -0
- package/dist/helpers/get-page-urls.d.ts.map +1 -0
- package/dist/helpers/get-page-urls.js +236 -0
- package/dist/helpers/get-page-urls.js.map +1 -0
- package/dist/helpers/html-to-markdown.d.ts +7 -0
- package/dist/helpers/html-to-markdown.d.ts.map +1 -0
- package/dist/helpers/html-to-markdown.js +11 -0
- package/dist/helpers/html-to-markdown.js.map +1 -0
- package/dist/helpers/index.d.ts +5 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +4 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/to-md-urls.d.ts +7 -0
- package/dist/helpers/to-md-urls.d.ts.map +1 -0
- package/dist/helpers/to-md-urls.js +24 -0
- package/dist/helpers/to-md-urls.js.map +1 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +21 -14
- package/dist/runner.js.map +1 -1
- package/dist/types.d.ts +13 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
|
@@ -1,10 +1,205 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
|
|
3
|
+
function parseCacheControl(header) {
|
|
4
|
+
if (!header)
|
|
5
|
+
return { maxAge: null, sMaxAge: null, mustRevalidate: false, noCache: false, noStore: false };
|
|
6
|
+
const directives = header
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.split(',')
|
|
9
|
+
.map((d) => d.trim());
|
|
10
|
+
let maxAge = null;
|
|
11
|
+
let sMaxAge = null;
|
|
12
|
+
let mustRevalidate = false;
|
|
13
|
+
let noCache = false;
|
|
14
|
+
let noStore = false;
|
|
15
|
+
for (const d of directives) {
|
|
16
|
+
if (d.startsWith('max-age=')) {
|
|
17
|
+
maxAge = parseInt(d.split('=')[1], 10);
|
|
18
|
+
if (isNaN(maxAge))
|
|
19
|
+
maxAge = null;
|
|
20
|
+
}
|
|
21
|
+
else if (d.startsWith('s-maxage=')) {
|
|
22
|
+
sMaxAge = parseInt(d.split('=')[1], 10);
|
|
23
|
+
if (isNaN(sMaxAge))
|
|
24
|
+
sMaxAge = null;
|
|
25
|
+
}
|
|
26
|
+
else if (d === 'must-revalidate') {
|
|
27
|
+
mustRevalidate = true;
|
|
28
|
+
}
|
|
29
|
+
else if (d === 'no-cache') {
|
|
30
|
+
noCache = true;
|
|
31
|
+
}
|
|
32
|
+
else if (d === 'no-store') {
|
|
33
|
+
noStore = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { maxAge, sMaxAge, mustRevalidate, noCache, noStore };
|
|
37
|
+
}
|
|
38
|
+
function classifyCache(result) {
|
|
39
|
+
// no-cache or no-store = always fresh
|
|
40
|
+
if (result.noCache || result.noStore)
|
|
41
|
+
return 'pass';
|
|
42
|
+
// must-revalidate with a revalidation mechanism is good
|
|
43
|
+
if (result.mustRevalidate && (result.etag || result.lastModified))
|
|
44
|
+
return 'pass';
|
|
45
|
+
const effective = result.effectiveMaxAge;
|
|
46
|
+
if (effective === null) {
|
|
47
|
+
// No cache-related headers at all
|
|
48
|
+
if (!result.cacheControl && !result.expires && !result.etag && !result.lastModified) {
|
|
49
|
+
return 'fail';
|
|
50
|
+
}
|
|
51
|
+
// Has ETag/Last-Modified but no max-age — still ok (browser will revalidate)
|
|
52
|
+
if (result.etag || result.lastModified)
|
|
53
|
+
return 'pass';
|
|
54
|
+
return 'fail';
|
|
55
|
+
}
|
|
56
|
+
if (effective <= 3600)
|
|
57
|
+
return 'pass';
|
|
58
|
+
if (effective <= 86400)
|
|
59
|
+
return 'warn';
|
|
60
|
+
return 'fail';
|
|
61
|
+
}
|
|
62
|
+
function getEffectiveMaxAge(parsed, expires) {
|
|
63
|
+
// s-maxage takes precedence over max-age
|
|
64
|
+
if (parsed.sMaxAge !== null)
|
|
65
|
+
return parsed.sMaxAge;
|
|
66
|
+
if (parsed.maxAge !== null)
|
|
67
|
+
return parsed.maxAge;
|
|
68
|
+
// Fall back to Expires header
|
|
69
|
+
if (expires) {
|
|
70
|
+
try {
|
|
71
|
+
const expiresMs = new Date(expires).getTime();
|
|
72
|
+
const nowMs = Date.now();
|
|
73
|
+
if (!isNaN(expiresMs)) {
|
|
74
|
+
return Math.max(0, Math.round((expiresMs - nowMs) / 1000));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// invalid date
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function worstStatus(statuses) {
|
|
84
|
+
if (statuses.includes('fail'))
|
|
85
|
+
return 'fail';
|
|
86
|
+
if (statuses.includes('warn'))
|
|
87
|
+
return 'warn';
|
|
88
|
+
return 'pass';
|
|
89
|
+
}
|
|
90
|
+
async function check(ctx) {
|
|
91
|
+
const id = 'cache-header-hygiene';
|
|
92
|
+
const category = 'observability';
|
|
93
|
+
// Collect URLs to check: llms.txt files + sampled page URLs
|
|
94
|
+
const urlsToCheck = [];
|
|
95
|
+
// llms.txt URLs
|
|
96
|
+
const existsResult = ctx.previousResults.get('llms-txt-exists');
|
|
97
|
+
const discovered = (existsResult?.details?.discoveredFiles ?? []);
|
|
98
|
+
for (const file of discovered) {
|
|
99
|
+
urlsToCheck.push(file.url);
|
|
100
|
+
}
|
|
101
|
+
// Page URLs
|
|
102
|
+
const { urls: pageUrls, totalPages, sampled, warnings } = await discoverAndSamplePages(ctx);
|
|
103
|
+
for (const url of pageUrls) {
|
|
104
|
+
if (!urlsToCheck.includes(url)) {
|
|
105
|
+
urlsToCheck.push(url);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const results = [];
|
|
109
|
+
const concurrency = ctx.options.maxConcurrency;
|
|
110
|
+
for (let i = 0; i < urlsToCheck.length; i += concurrency) {
|
|
111
|
+
const batch = urlsToCheck.slice(i, i + concurrency);
|
|
112
|
+
const batchResults = await Promise.all(batch.map(async (url) => {
|
|
113
|
+
try {
|
|
114
|
+
const response = await ctx.http.fetch(url);
|
|
115
|
+
const ccHeader = response.headers.get('cache-control');
|
|
116
|
+
const parsed = parseCacheControl(ccHeader);
|
|
117
|
+
const etag = response.headers.get('etag');
|
|
118
|
+
const lastModified = response.headers.get('last-modified');
|
|
119
|
+
const expires = response.headers.get('expires');
|
|
120
|
+
const effectiveMaxAge = getEffectiveMaxAge(parsed, expires);
|
|
121
|
+
const partial = {
|
|
122
|
+
url,
|
|
123
|
+
cacheControl: ccHeader,
|
|
124
|
+
maxAge: parsed.maxAge,
|
|
125
|
+
sMaxAge: parsed.sMaxAge,
|
|
126
|
+
mustRevalidate: parsed.mustRevalidate,
|
|
127
|
+
noCache: parsed.noCache,
|
|
128
|
+
noStore: parsed.noStore,
|
|
129
|
+
etag,
|
|
130
|
+
lastModified,
|
|
131
|
+
expires,
|
|
132
|
+
effectiveMaxAge,
|
|
133
|
+
};
|
|
134
|
+
return { ...partial, status: classifyCache(partial) };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return {
|
|
138
|
+
url,
|
|
139
|
+
cacheControl: null,
|
|
140
|
+
maxAge: null,
|
|
141
|
+
sMaxAge: null,
|
|
142
|
+
mustRevalidate: false,
|
|
143
|
+
noCache: false,
|
|
144
|
+
noStore: false,
|
|
145
|
+
etag: null,
|
|
146
|
+
lastModified: null,
|
|
147
|
+
expires: null,
|
|
148
|
+
effectiveMaxAge: null,
|
|
149
|
+
status: 'fail',
|
|
150
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
results.push(...batchResults);
|
|
155
|
+
}
|
|
156
|
+
const successful = results.filter((r) => !r.error);
|
|
157
|
+
const fetchErrors = results.filter((r) => r.error).length;
|
|
158
|
+
if (successful.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
category,
|
|
162
|
+
status: 'fail',
|
|
163
|
+
message: `Could not fetch any endpoints to check cache headers${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
|
|
164
|
+
details: {
|
|
165
|
+
testedEndpoints: results.length,
|
|
166
|
+
fetchErrors,
|
|
167
|
+
endpointResults: results,
|
|
168
|
+
discoveryWarnings: warnings,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const overallStatus = worstStatus(successful.map((r) => r.status));
|
|
173
|
+
const passBucket = successful.filter((r) => r.status === 'pass').length;
|
|
174
|
+
const warnBucket = successful.filter((r) => r.status === 'warn').length;
|
|
175
|
+
const failBucket = successful.filter((r) => r.status === 'fail').length;
|
|
176
|
+
const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
|
|
177
|
+
let message;
|
|
178
|
+
if (overallStatus === 'pass') {
|
|
179
|
+
message = `All ${successful.length} endpoints have appropriate cache headers${suffix}`;
|
|
180
|
+
}
|
|
181
|
+
else if (overallStatus === 'warn') {
|
|
182
|
+
message = `${warnBucket} of ${successful.length} endpoints have moderate cache lifetimes (1–24 hours)${suffix}`;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
message = `${failBucket} of ${successful.length} endpoints have aggressive caching or missing cache headers${suffix}`;
|
|
186
|
+
}
|
|
3
187
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status:
|
|
7
|
-
message
|
|
188
|
+
id,
|
|
189
|
+
category,
|
|
190
|
+
status: overallStatus,
|
|
191
|
+
message,
|
|
192
|
+
details: {
|
|
193
|
+
totalPages,
|
|
194
|
+
testedEndpoints: results.length,
|
|
195
|
+
sampled,
|
|
196
|
+
passBucket,
|
|
197
|
+
warnBucket,
|
|
198
|
+
failBucket,
|
|
199
|
+
fetchErrors,
|
|
200
|
+
endpointResults: results,
|
|
201
|
+
discoveryWarnings: warnings,
|
|
202
|
+
},
|
|
8
203
|
};
|
|
9
204
|
}
|
|
10
205
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-header-hygiene.js","sourceRoot":"","sources":["../../../src/checks/observability/cache-header-hygiene.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-header-hygiene.js","sourceRoot":"","sources":["../../../src/checks/observability/cache-header-hygiene.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,166 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { looksLikeHtml } from '../../helpers/detect-markdown.js';
|
|
3
|
+
import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
|
|
4
|
+
import { htmlToMarkdown } from '../../helpers/html-to-markdown.js';
|
|
5
|
+
const CSS_PATTERN = /[{}\s]*[a-z-]+\s*:\s*[^;]+;/;
|
|
6
|
+
const JS_PATTERNS = [/^\s*(function|var|const|let|import|export)\b/, /^\s*\/\//, /[{};]\s*$/];
|
|
7
|
+
const NAV_MAX_LENGTH = 40;
|
|
8
|
+
/**
|
|
9
|
+
* Find the character position where meaningful content begins in converted markdown.
|
|
10
|
+
* Meaningful content is a heading or a prose paragraph (not CSS, JS, or short nav text).
|
|
11
|
+
*/
|
|
12
|
+
function findContentStart(markdown) {
|
|
13
|
+
const lines = markdown.split('\n');
|
|
14
|
+
let charPos = 0;
|
|
15
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
16
|
+
const line = lines[idx];
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (trimmed.length === 0) {
|
|
19
|
+
charPos += line.length + 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
// ATX heading: starts with # at beginning of line
|
|
23
|
+
if (/^#{1,6}\s+\S/.test(trimmed)) {
|
|
24
|
+
return charPos;
|
|
25
|
+
}
|
|
26
|
+
// Setext heading: current line is text, next line is === or ---
|
|
27
|
+
const nextLine = idx + 1 < lines.length ? lines[idx + 1].trim() : '';
|
|
28
|
+
if (/^[=-]+$/.test(nextLine) && nextLine.length >= 2 && trimmed.length > 0) {
|
|
29
|
+
return charPos;
|
|
30
|
+
}
|
|
31
|
+
// Skip CSS-like lines
|
|
32
|
+
if (CSS_PATTERN.test(trimmed)) {
|
|
33
|
+
charPos += line.length + 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Skip JS-like lines
|
|
37
|
+
if (JS_PATTERNS.some((p) => p.test(trimmed))) {
|
|
38
|
+
charPos += line.length + 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Skip very short nav-like tokens (e.g., "Home", "Docs", "API")
|
|
42
|
+
if (trimmed.length <= NAV_MAX_LENGTH && !/[.!?]/.test(trimmed) && !trimmed.includes(' ')) {
|
|
43
|
+
charPos += line.length + 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Prose-like paragraph: contains spaces (multiple words) and is reasonably long
|
|
47
|
+
if (trimmed.length > NAV_MAX_LENGTH || (trimmed.includes(' ') && trimmed.length > 20)) {
|
|
48
|
+
return charPos;
|
|
49
|
+
}
|
|
50
|
+
charPos += line.length + 1;
|
|
51
|
+
}
|
|
52
|
+
// If nothing matched, content starts at the end (all boilerplate)
|
|
53
|
+
return charPos;
|
|
54
|
+
}
|
|
55
|
+
function positionStatus(percent) {
|
|
56
|
+
if (percent <= 10)
|
|
57
|
+
return 'pass';
|
|
58
|
+
if (percent <= 50)
|
|
59
|
+
return 'warn';
|
|
60
|
+
return 'fail';
|
|
61
|
+
}
|
|
62
|
+
function worstStatus(statuses) {
|
|
63
|
+
if (statuses.includes('fail'))
|
|
64
|
+
return 'fail';
|
|
65
|
+
if (statuses.includes('warn'))
|
|
66
|
+
return 'warn';
|
|
67
|
+
return 'pass';
|
|
68
|
+
}
|
|
69
|
+
async function check(ctx) {
|
|
70
|
+
const id = 'content-start-position';
|
|
71
|
+
const category = 'page-size';
|
|
72
|
+
const { urls: pageUrls, totalPages, sampled: wasSampled, warnings, } = await discoverAndSamplePages(ctx);
|
|
73
|
+
const results = [];
|
|
74
|
+
const concurrency = ctx.options.maxConcurrency;
|
|
75
|
+
for (let i = 0; i < pageUrls.length; i += concurrency) {
|
|
76
|
+
const batch = pageUrls.slice(i, i + concurrency);
|
|
77
|
+
const batchResults = await Promise.all(batch.map(async (url) => {
|
|
78
|
+
try {
|
|
79
|
+
const response = await ctx.http.fetch(url);
|
|
80
|
+
const body = await response.text();
|
|
81
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
82
|
+
const isMarkdownType = contentType.includes('text/markdown') || contentType.includes('text/plain');
|
|
83
|
+
const isHtml = !isMarkdownType && (contentType.includes('text/html') || looksLikeHtml(body));
|
|
84
|
+
const markdown = isHtml ? htmlToMarkdown(body) : body;
|
|
85
|
+
const totalChars = markdown.length;
|
|
86
|
+
const contentStartChar = findContentStart(markdown);
|
|
87
|
+
const contentStartPercent = totalChars > 0 ? Math.round((contentStartChar / totalChars) * 100) : 0;
|
|
88
|
+
return {
|
|
89
|
+
url,
|
|
90
|
+
contentStartChar,
|
|
91
|
+
totalChars,
|
|
92
|
+
contentStartPercent,
|
|
93
|
+
status: positionStatus(contentStartPercent),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
url,
|
|
99
|
+
contentStartChar: 0,
|
|
100
|
+
totalChars: 0,
|
|
101
|
+
contentStartPercent: 100,
|
|
102
|
+
status: 'fail',
|
|
103
|
+
error: err instanceof Error ? err.message : String(err),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
results.push(...batchResults);
|
|
108
|
+
}
|
|
109
|
+
const successful = results.filter((r) => !r.error);
|
|
110
|
+
const fetchErrors = results.filter((r) => r.error).length;
|
|
111
|
+
if (successful.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
category,
|
|
115
|
+
status: 'fail',
|
|
116
|
+
message: `Could not fetch any pages to analyze${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
|
|
117
|
+
details: {
|
|
118
|
+
totalPages,
|
|
119
|
+
testedPages: results.length,
|
|
120
|
+
sampled: wasSampled,
|
|
121
|
+
fetchErrors,
|
|
122
|
+
pageResults: results,
|
|
123
|
+
discoveryWarnings: warnings,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const percents = successful.map((r) => r.contentStartPercent);
|
|
128
|
+
const medianPercent = percents.sort((a, b) => a - b)[Math.floor(percents.length / 2)];
|
|
129
|
+
const maxPercent = Math.max(...percents);
|
|
130
|
+
const overallStatus = worstStatus(successful.map((r) => r.status));
|
|
131
|
+
const pageLabel = wasSampled ? 'sampled pages' : 'pages';
|
|
132
|
+
const passBucket = successful.filter((r) => r.status === 'pass').length;
|
|
133
|
+
const warnBucket = successful.filter((r) => r.status === 'warn').length;
|
|
134
|
+
const failBucket = successful.filter((r) => r.status === 'fail').length;
|
|
135
|
+
const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
|
|
136
|
+
let message;
|
|
137
|
+
if (overallStatus === 'pass') {
|
|
138
|
+
message = `Content starts within first 10% on all ${successful.length} ${pageLabel} (median ${medianPercent}%)${suffix}`;
|
|
139
|
+
}
|
|
140
|
+
else if (overallStatus === 'warn') {
|
|
141
|
+
message = `${warnBucket} of ${successful.length} ${pageLabel} have content starting at 10–50% (worst ${maxPercent}%)${suffix}`;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
message = `${failBucket} of ${successful.length} ${pageLabel} have content starting past 50% (worst ${maxPercent}%)${suffix}`;
|
|
145
|
+
}
|
|
3
146
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status:
|
|
7
|
-
message
|
|
147
|
+
id,
|
|
148
|
+
category,
|
|
149
|
+
status: overallStatus,
|
|
150
|
+
message,
|
|
151
|
+
details: {
|
|
152
|
+
totalPages,
|
|
153
|
+
testedPages: results.length,
|
|
154
|
+
sampled: wasSampled,
|
|
155
|
+
medianPercent,
|
|
156
|
+
maxPercent,
|
|
157
|
+
passBucket,
|
|
158
|
+
warnBucket,
|
|
159
|
+
failBucket,
|
|
160
|
+
fetchErrors,
|
|
161
|
+
pageResults: results,
|
|
162
|
+
discoveryWarnings: warnings,
|
|
163
|
+
},
|
|
8
164
|
};
|
|
9
165
|
}
|
|
10
166
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-start-position.js","sourceRoot":"","sources":["../../../src/checks/page-size/content-start-position.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"content-start-position.js","sourceRoot":"","sources":["../../../src/checks/page-size/content-start-position.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,132 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { looksLikeHtml } from '../../helpers/detect-markdown.js';
|
|
3
|
+
import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
|
|
4
|
+
import { htmlToMarkdown } from '../../helpers/html-to-markdown.js';
|
|
5
|
+
function sizeStatus(chars, pass, fail) {
|
|
6
|
+
if (chars <= pass)
|
|
7
|
+
return 'pass';
|
|
8
|
+
if (chars <= fail)
|
|
9
|
+
return 'warn';
|
|
10
|
+
return 'fail';
|
|
11
|
+
}
|
|
12
|
+
function worstStatus(statuses) {
|
|
13
|
+
if (statuses.includes('fail'))
|
|
14
|
+
return 'fail';
|
|
15
|
+
if (statuses.includes('warn'))
|
|
16
|
+
return 'warn';
|
|
17
|
+
return 'pass';
|
|
18
|
+
}
|
|
19
|
+
function formatSize(chars) {
|
|
20
|
+
if (chars >= 1000)
|
|
21
|
+
return `${Math.round(chars / 1000)}K`;
|
|
22
|
+
return String(chars);
|
|
23
|
+
}
|
|
24
|
+
async function check(ctx) {
|
|
25
|
+
const id = 'page-size-html';
|
|
26
|
+
const category = 'page-size';
|
|
27
|
+
const { pass: passThreshold, fail: failThreshold } = ctx.options.thresholds;
|
|
28
|
+
const { urls: pageUrls, totalPages, sampled: wasSampled, warnings, } = await discoverAndSamplePages(ctx);
|
|
29
|
+
const results = [];
|
|
30
|
+
const concurrency = ctx.options.maxConcurrency;
|
|
31
|
+
for (let i = 0; i < pageUrls.length; i += concurrency) {
|
|
32
|
+
const batch = pageUrls.slice(i, i + concurrency);
|
|
33
|
+
const batchResults = await Promise.all(batch.map(async (url) => {
|
|
34
|
+
try {
|
|
35
|
+
const response = await ctx.http.fetch(url);
|
|
36
|
+
const body = await response.text();
|
|
37
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
38
|
+
const isMarkdownType = contentType.includes('text/markdown') || contentType.includes('text/plain');
|
|
39
|
+
const isHtml = !isMarkdownType && (contentType.includes('text/html') || looksLikeHtml(body));
|
|
40
|
+
// Skip conversion if the response is already markdown
|
|
41
|
+
const html = isHtml ? body : '';
|
|
42
|
+
const htmlChars = html.length;
|
|
43
|
+
const converted = isHtml ? htmlToMarkdown(body) : body;
|
|
44
|
+
const convertedChars = converted.length;
|
|
45
|
+
const ratio = htmlChars > 0 ? Math.round((1 - convertedChars / htmlChars) * 100) : 0;
|
|
46
|
+
return {
|
|
47
|
+
url,
|
|
48
|
+
htmlCharacters: htmlChars,
|
|
49
|
+
convertedCharacters: convertedChars,
|
|
50
|
+
conversionRatio: ratio,
|
|
51
|
+
status: sizeStatus(convertedChars, passThreshold, failThreshold),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return {
|
|
56
|
+
url,
|
|
57
|
+
htmlCharacters: 0,
|
|
58
|
+
convertedCharacters: 0,
|
|
59
|
+
conversionRatio: 0,
|
|
60
|
+
status: 'fail',
|
|
61
|
+
error: err instanceof Error ? err.message : String(err),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
results.push(...batchResults);
|
|
66
|
+
}
|
|
67
|
+
const successful = results.filter((r) => !r.error);
|
|
68
|
+
const fetchErrors = results.filter((r) => r.error).length;
|
|
69
|
+
const rateLimited = results.filter((r) => r.error && r.error.includes('429')).length;
|
|
70
|
+
if (successful.length === 0) {
|
|
71
|
+
const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
category,
|
|
75
|
+
status: 'fail',
|
|
76
|
+
message: `Could not fetch any pages to measure${suffix}`,
|
|
77
|
+
details: {
|
|
78
|
+
totalPages,
|
|
79
|
+
testedPages: results.length,
|
|
80
|
+
sampled: wasSampled,
|
|
81
|
+
fetchErrors,
|
|
82
|
+
rateLimited,
|
|
83
|
+
pageResults: results,
|
|
84
|
+
discoveryWarnings: warnings,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const convertedSizes = successful.map((r) => r.convertedCharacters).sort((a, b) => a - b);
|
|
89
|
+
const median = convertedSizes[Math.floor(convertedSizes.length / 2)];
|
|
90
|
+
const max = convertedSizes[convertedSizes.length - 1];
|
|
91
|
+
const avgRatio = Math.round(successful.reduce((sum, r) => sum + r.conversionRatio, 0) / successful.length);
|
|
92
|
+
const overallStatus = worstStatus(successful.map((r) => r.status));
|
|
93
|
+
const pageLabel = wasSampled ? 'sampled pages' : 'pages';
|
|
94
|
+
const passBucket = successful.filter((r) => r.status === 'pass').length;
|
|
95
|
+
const warnBucket = successful.filter((r) => r.status === 'warn').length;
|
|
96
|
+
const failBucket = successful.filter((r) => r.status === 'fail').length;
|
|
97
|
+
const suffix = (fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '') +
|
|
98
|
+
(rateLimited > 0 ? `; ${rateLimited} rate-limited (HTTP 429)` : '');
|
|
99
|
+
let message;
|
|
100
|
+
if (overallStatus === 'pass') {
|
|
101
|
+
message = `All ${successful.length} ${pageLabel} convert under ${formatSize(passThreshold)} chars (median ${formatSize(median)}, ${avgRatio}% boilerplate)${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
else if (overallStatus === 'warn') {
|
|
104
|
+
message = `${warnBucket} of ${successful.length} ${pageLabel} convert to ${formatSize(passThreshold)}–${formatSize(failThreshold)} chars (max ${formatSize(max)}, ${avgRatio}% boilerplate)${suffix}`;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
message = `${failBucket} of ${successful.length} ${pageLabel} convert to over ${formatSize(failThreshold)} chars (max ${formatSize(max)}, ${avgRatio}% boilerplate)${suffix}`;
|
|
108
|
+
}
|
|
3
109
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status:
|
|
7
|
-
message
|
|
110
|
+
id,
|
|
111
|
+
category,
|
|
112
|
+
status: overallStatus,
|
|
113
|
+
message,
|
|
114
|
+
details: {
|
|
115
|
+
totalPages,
|
|
116
|
+
testedPages: results.length,
|
|
117
|
+
sampled: wasSampled,
|
|
118
|
+
median,
|
|
119
|
+
max,
|
|
120
|
+
avgBoilerplatePercent: avgRatio,
|
|
121
|
+
passBucket,
|
|
122
|
+
warnBucket,
|
|
123
|
+
failBucket,
|
|
124
|
+
fetchErrors,
|
|
125
|
+
rateLimited,
|
|
126
|
+
thresholds: { pass: passThreshold, fail: failThreshold },
|
|
127
|
+
pageResults: results,
|
|
128
|
+
discoveryWarnings: warnings,
|
|
129
|
+
},
|
|
8
130
|
};
|
|
9
131
|
}
|
|
10
132
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-size-html.js","sourceRoot":"","sources":["../../../src/checks/page-size/page-size-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"page-size-html.js","sourceRoot":"","sources":["../../../src/checks/page-size/page-size-html.ts"],"names":[],"mappings":"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"}
|