@nuasite/checks 0.16.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/dist/types/check-runner.d.ts +16 -0
- package/dist/types/check-runner.d.ts.map +1 -0
- package/dist/types/checks/accessibility/aria-landmarks-check.d.ts +3 -0
- package/dist/types/checks/accessibility/aria-landmarks-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/form-label-check.d.ts +3 -0
- package/dist/types/checks/accessibility/form-label-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/index.d.ts +6 -0
- package/dist/types/checks/accessibility/index.d.ts.map +1 -0
- package/dist/types/checks/accessibility/lang-attribute-check.d.ts +3 -0
- package/dist/types/checks/accessibility/lang-attribute-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/link-text-check.d.ts +3 -0
- package/dist/types/checks/accessibility/link-text-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/tabindex-check.d.ts +3 -0
- package/dist/types/checks/accessibility/tabindex-check.d.ts.map +1 -0
- package/dist/types/checks/geo/agents-md-check.d.ts.map +1 -0
- package/dist/types/checks/geo/content-quality-check.d.ts +4 -0
- package/dist/types/checks/geo/content-quality-check.d.ts.map +1 -0
- package/dist/types/checks/geo/index.d.ts +3 -0
- package/dist/types/checks/geo/index.d.ts.map +1 -0
- package/dist/types/checks/geo/llms-txt-check.d.ts +3 -0
- package/dist/types/checks/geo/llms-txt-check.d.ts.map +1 -0
- package/dist/types/checks/performance/html-size-check.d.ts +3 -0
- package/dist/types/checks/performance/html-size-check.d.ts.map +1 -0
- package/dist/types/checks/performance/image-optimization-check.d.ts +4 -0
- package/dist/types/checks/performance/image-optimization-check.d.ts.map +1 -0
- package/dist/types/checks/performance/index.d.ts +7 -0
- package/dist/types/checks/performance/index.d.ts.map +1 -0
- package/dist/types/checks/performance/inline-size-check.d.ts +3 -0
- package/dist/types/checks/performance/inline-size-check.d.ts.map +1 -0
- package/dist/types/checks/performance/lazy-loading-check.d.ts +3 -0
- package/dist/types/checks/performance/lazy-loading-check.d.ts.map +1 -0
- package/dist/types/checks/performance/render-blocking-check.d.ts +3 -0
- package/dist/types/checks/performance/render-blocking-check.d.ts.map +1 -0
- package/dist/types/checks/performance/total-requests-check.d.ts +3 -0
- package/dist/types/checks/performance/total-requests-check.d.ts.map +1 -0
- package/dist/types/checks/seo/broken-internal-links-check.d.ts +3 -0
- package/dist/types/checks/seo/broken-internal-links-check.d.ts.map +1 -0
- package/dist/types/checks/seo/canonical-check.d.ts +5 -0
- package/dist/types/checks/seo/canonical-check.d.ts.map +1 -0
- package/dist/types/checks/seo/description-check.d.ts +4 -0
- package/dist/types/checks/seo/description-check.d.ts.map +1 -0
- package/dist/types/checks/seo/heading-hierarchy-check.d.ts +5 -0
- package/dist/types/checks/seo/heading-hierarchy-check.d.ts.map +1 -0
- package/dist/types/checks/seo/image-alt-check.d.ts +3 -0
- package/dist/types/checks/seo/image-alt-check.d.ts.map +1 -0
- package/dist/types/checks/seo/image-alt-quality-check.d.ts +3 -0
- package/dist/types/checks/seo/image-alt-quality-check.d.ts.map +1 -0
- package/dist/types/checks/seo/index.d.ts +15 -0
- package/dist/types/checks/seo/index.d.ts.map +1 -0
- package/dist/types/checks/seo/json-ld-check.d.ts +3 -0
- package/dist/types/checks/seo/json-ld-check.d.ts.map +1 -0
- package/dist/types/checks/seo/meta-duplicates-check.d.ts +3 -0
- package/dist/types/checks/seo/meta-duplicates-check.d.ts.map +1 -0
- package/dist/types/checks/seo/noindex-check.d.ts +3 -0
- package/dist/types/checks/seo/noindex-check.d.ts.map +1 -0
- package/dist/types/checks/seo/open-graph-check.d.ts +5 -0
- package/dist/types/checks/seo/open-graph-check.d.ts.map +1 -0
- package/dist/types/checks/seo/sitemap-robots-check.d.ts +4 -0
- package/dist/types/checks/seo/sitemap-robots-check.d.ts.map +1 -0
- package/dist/types/checks/seo/title-check.d.ts +5 -0
- package/dist/types/checks/seo/title-check.d.ts.map +1 -0
- package/dist/types/checks/seo/twitter-card-check.d.ts +3 -0
- package/dist/types/checks/seo/twitter-card-check.d.ts.map +1 -0
- package/dist/types/checks/seo/viewport-check.d.ts +3 -0
- package/dist/types/checks/seo/viewport-check.d.ts.map +1 -0
- package/dist/types/checks-integration.d.ts +4 -0
- package/dist/types/checks-integration.d.ts.map +1 -0
- package/dist/types/config.d.ts +3 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/html-analyzer.d.ts +11 -0
- package/dist/types/html-analyzer.d.ts.map +1 -0
- package/dist/types/i18n/poor-texts.d.ts +14 -0
- package/dist/types/i18n/poor-texts.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/register.d.ts +4 -0
- package/dist/types/register.d.ts.map +1 -0
- package/dist/types/report.d.ts +5 -0
- package/dist/types/report.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dist/types/types.d.ts +220 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +49 -0
- package/src/check-runner.ts +92 -0
- package/src/checks/accessibility/aria-landmarks-check.ts +23 -0
- package/src/checks/accessibility/form-label-check.ts +29 -0
- package/src/checks/accessibility/index.ts +5 -0
- package/src/checks/accessibility/lang-attribute-check.ts +22 -0
- package/src/checks/accessibility/link-text-check.ts +30 -0
- package/src/checks/accessibility/tabindex-check.ts +28 -0
- package/src/checks/geo/content-quality-check.ts +48 -0
- package/src/checks/geo/index.ts +2 -0
- package/src/checks/geo/llms-txt-check.ts +37 -0
- package/src/checks/performance/html-size-check.ts +26 -0
- package/src/checks/performance/image-optimization-check.ts +82 -0
- package/src/checks/performance/index.ts +6 -0
- package/src/checks/performance/inline-size-check.ts +29 -0
- package/src/checks/performance/lazy-loading-check.ts +29 -0
- package/src/checks/performance/render-blocking-check.ts +27 -0
- package/src/checks/performance/total-requests-check.ts +27 -0
- package/src/checks/seo/broken-internal-links-check.ts +49 -0
- package/src/checks/seo/canonical-check.ts +77 -0
- package/src/checks/seo/description-check.ts +55 -0
- package/src/checks/seo/heading-hierarchy-check.ts +76 -0
- package/src/checks/seo/image-alt-check.ts +28 -0
- package/src/checks/seo/image-alt-quality-check.ts +31 -0
- package/src/checks/seo/index.ts +14 -0
- package/src/checks/seo/json-ld-check.ts +26 -0
- package/src/checks/seo/meta-duplicates-check.ts +44 -0
- package/src/checks/seo/noindex-check.ts +22 -0
- package/src/checks/seo/open-graph-check.ts +55 -0
- package/src/checks/seo/sitemap-robots-check.ts +55 -0
- package/src/checks/seo/title-check.ts +63 -0
- package/src/checks/seo/twitter-card-check.ts +22 -0
- package/src/checks/seo/viewport-check.ts +22 -0
- package/src/checks-integration.ts +126 -0
- package/src/config.ts +27 -0
- package/src/html-analyzer.ts +325 -0
- package/src/i18n/poor-texts.ts +66 -0
- package/src/index.ts +22 -0
- package/src/register.ts +110 -0
- package/src/report.ts +78 -0
- package/src/tsconfig.json +6 -0
- package/src/types.ts +244 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-keyed collections of generic/non-descriptive text patterns.
|
|
3
|
+
* Used by accessibility and SEO checks to detect poor link text and image alt text.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const poorLinkTexts: Record<string, string[]> = {
|
|
7
|
+
en: ['click here', 'read more', 'here', 'link', 'more', 'learn more'],
|
|
8
|
+
es: ['haz clic aquí', 'clic aquí', 'leer más', 'aquí', 'enlace', 'más', 'más información'],
|
|
9
|
+
fr: ['cliquez ici', 'lire la suite', 'en savoir plus', 'ici', 'lien', 'plus'],
|
|
10
|
+
de: ['hier klicken', 'klicken sie hier', 'mehr lesen', 'weiterlesen', 'hier', 'mehr', 'mehr erfahren'],
|
|
11
|
+
it: ['clicca qui', 'leggi di più', 'continua a leggere', 'qui', 'collegamento', 'più'],
|
|
12
|
+
pt: ['clique aqui', 'leia mais', 'saiba mais', 'aqui', 'mais'],
|
|
13
|
+
nl: ['klik hier', 'lees meer', 'meer'],
|
|
14
|
+
cs: ['klikněte zde', 'klikni zde', 'čtěte dále', 'více', 'zde', 'odkaz', 'zjistit více'],
|
|
15
|
+
ko: ['여기를 클릭', '더 보기', '자세히 보기', '여기', '링크', '더보기', '클릭'],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const poorImageAlts: Record<string, string[]> = {
|
|
19
|
+
en: ['image', 'photo', 'picture', 'img', 'untitled', 'screenshot', 'banner', 'logo', 'icon', 'graphic', 'thumbnail'],
|
|
20
|
+
es: ['imagen', 'foto', 'fotografía', 'captura de pantalla', 'logotipo', 'ícono'],
|
|
21
|
+
fr: ['image', 'photo', "capture d'écran", 'logo', 'icône', 'graphique', 'vignette'],
|
|
22
|
+
de: ['bild', 'foto', 'grafik', 'bildschirmfoto', 'logo', 'symbol'],
|
|
23
|
+
it: ['immagine', 'foto', 'fotografia', 'schermata', 'logo', 'icona'],
|
|
24
|
+
pt: ['imagem', 'foto', 'fotografia', 'captura de tela', 'logotipo', 'ícone'],
|
|
25
|
+
nl: ['afbeelding', 'foto', 'schermafbeelding', 'logo', 'icoon'],
|
|
26
|
+
cs: ['obrázek', 'foto', 'fotografie', 'snímek obrazovky', 'logo', 'ikona'],
|
|
27
|
+
ko: ['이미지', '사진', '스크린샷', '로고', '아이콘', '그래픽', '썸네일'],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cache = new WeakMap<Record<string, string[]>, Map<string, Set<string>>>()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build a Set of poor texts, optionally filtered by page locale.
|
|
34
|
+
* When a locale is provided, returns texts for that language + English as fallback.
|
|
35
|
+
* When no locale is provided or locale is unrecognized, returns English texts only.
|
|
36
|
+
* Results are cached by dictionary + locale.
|
|
37
|
+
*/
|
|
38
|
+
export function buildPoorTextSet(texts: Record<string, string[]>, locale?: string): Set<string> {
|
|
39
|
+
const key = locale?.toLowerCase().split('-')[0] ?? '*'
|
|
40
|
+
|
|
41
|
+
let localeMap = cache.get(texts)
|
|
42
|
+
if (!localeMap) {
|
|
43
|
+
localeMap = new Map()
|
|
44
|
+
cache.set(texts, localeMap)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cached = localeMap.get(key)
|
|
48
|
+
if (cached) return cached
|
|
49
|
+
|
|
50
|
+
const en = texts.en ?? []
|
|
51
|
+
let result: Set<string>
|
|
52
|
+
|
|
53
|
+
if (key === '*') {
|
|
54
|
+
const set = new Set<string>()
|
|
55
|
+
for (const arr of Object.values(texts)) {
|
|
56
|
+
for (const s of arr) set.add(s)
|
|
57
|
+
}
|
|
58
|
+
result = set
|
|
59
|
+
} else {
|
|
60
|
+
const matched = texts[key]
|
|
61
|
+
result = matched ? new Set([...matched, ...en]) : new Set(en)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
localeMap.set(key, result)
|
|
65
|
+
return result
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { checks } from './checks-integration'
|
|
2
|
+
export { checks as default } from './checks-integration'
|
|
3
|
+
export type {
|
|
4
|
+
AccessibilityChecksConfig,
|
|
5
|
+
AiChecksConfig,
|
|
6
|
+
Check,
|
|
7
|
+
CheckDomain,
|
|
8
|
+
CheckIssue,
|
|
9
|
+
CheckMode,
|
|
10
|
+
CheckReport,
|
|
11
|
+
CheckResult,
|
|
12
|
+
CheckSeverity,
|
|
13
|
+
ChecksOptions,
|
|
14
|
+
ExtractedPageData,
|
|
15
|
+
GeoChecksConfig,
|
|
16
|
+
PageCheckContext,
|
|
17
|
+
PerformanceChecksConfig,
|
|
18
|
+
SeoChecksConfig,
|
|
19
|
+
SiteCheck,
|
|
20
|
+
SiteCheckContext,
|
|
21
|
+
SiteCheckIssue,
|
|
22
|
+
} from './types'
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CheckRunner } from './check-runner'
|
|
2
|
+
import type { ResolvedChecksOptions } from './types'
|
|
3
|
+
|
|
4
|
+
// SEO checks
|
|
5
|
+
import { createBrokenInternalLinksCheck } from './checks/seo/broken-internal-links-check'
|
|
6
|
+
import { createCanonicalInvalidCheck, createCanonicalMismatchCheck, createCanonicalMissingCheck } from './checks/seo/canonical-check'
|
|
7
|
+
import { createDescriptionLengthCheck, createDescriptionMissingCheck } from './checks/seo/description-check'
|
|
8
|
+
import { createHeadingSkipCheck, createMultipleH1Check, createNoH1Check } from './checks/seo/heading-hierarchy-check'
|
|
9
|
+
import { createImageAltMissingCheck } from './checks/seo/image-alt-check'
|
|
10
|
+
import { createImageAltQualityCheck } from './checks/seo/image-alt-quality-check'
|
|
11
|
+
import { createJsonLdInvalidCheck } from './checks/seo/json-ld-check'
|
|
12
|
+
import { createMetaDuplicateCheck } from './checks/seo/meta-duplicates-check'
|
|
13
|
+
import { createNoindexDetectedCheck } from './checks/seo/noindex-check'
|
|
14
|
+
import { createOgDescriptionCheck, createOgImageCheck, createOgTitleCheck } from './checks/seo/open-graph-check'
|
|
15
|
+
import { createRobotsTxtCheck, createSitemapXmlCheck } from './checks/seo/sitemap-robots-check'
|
|
16
|
+
import { createTitleEmptyCheck, createTitleLengthCheck, createTitleMissingCheck } from './checks/seo/title-check'
|
|
17
|
+
import { createTwitterCardCheck } from './checks/seo/twitter-card-check'
|
|
18
|
+
import { createViewportMissingCheck } from './checks/seo/viewport-check'
|
|
19
|
+
|
|
20
|
+
// GEO checks
|
|
21
|
+
import { createContentTooShortCheck, createInsufficientHeadingsCheck } from './checks/geo/content-quality-check'
|
|
22
|
+
import { createLlmsTxtCheck } from './checks/geo/llms-txt-check'
|
|
23
|
+
|
|
24
|
+
// Performance checks
|
|
25
|
+
import { createHtmlSizeCheck } from './checks/performance/html-size-check'
|
|
26
|
+
import { createImageFormatCheck, createImageSizeCheck } from './checks/performance/image-optimization-check'
|
|
27
|
+
import { createInlineSizeCheck } from './checks/performance/inline-size-check'
|
|
28
|
+
import { createLazyLoadingCheck } from './checks/performance/lazy-loading-check'
|
|
29
|
+
import { createRenderBlockingScriptCheck } from './checks/performance/render-blocking-check'
|
|
30
|
+
import { createTotalRequestsCheck } from './checks/performance/total-requests-check'
|
|
31
|
+
|
|
32
|
+
// Accessibility checks
|
|
33
|
+
import { createAriaLandmarksCheck } from './checks/accessibility/aria-landmarks-check'
|
|
34
|
+
import { createFormLabelCheck } from './checks/accessibility/form-label-check'
|
|
35
|
+
import { createLangAttributeCheck } from './checks/accessibility/lang-attribute-check'
|
|
36
|
+
import { createLinkTextCheck } from './checks/accessibility/link-text-check'
|
|
37
|
+
import { createTabindexCheck } from './checks/accessibility/tabindex-check'
|
|
38
|
+
|
|
39
|
+
export function registerAllChecks(runner: CheckRunner, options: ResolvedChecksOptions): void {
|
|
40
|
+
// SEO checks
|
|
41
|
+
if (options.seo !== false) {
|
|
42
|
+
const seo = options.seo
|
|
43
|
+
const titleMax = seo.titleMaxLength ?? 60
|
|
44
|
+
const descMax = seo.descriptionMaxLength ?? 160
|
|
45
|
+
const descMin = seo.descriptionMinLength ?? 50
|
|
46
|
+
|
|
47
|
+
runner.registerCheck(createTitleMissingCheck())
|
|
48
|
+
runner.registerCheck(createTitleEmptyCheck())
|
|
49
|
+
runner.registerCheck(createTitleLengthCheck(titleMax))
|
|
50
|
+
runner.registerCheck(createDescriptionMissingCheck())
|
|
51
|
+
runner.registerCheck(createDescriptionLengthCheck(descMin, descMax))
|
|
52
|
+
runner.registerCheck(createCanonicalMissingCheck())
|
|
53
|
+
runner.registerCheck(createCanonicalInvalidCheck())
|
|
54
|
+
runner.registerCheck(createCanonicalMismatchCheck())
|
|
55
|
+
runner.registerCheck(createJsonLdInvalidCheck())
|
|
56
|
+
runner.registerCheck(createMultipleH1Check())
|
|
57
|
+
runner.registerCheck(createNoH1Check())
|
|
58
|
+
runner.registerCheck(createHeadingSkipCheck())
|
|
59
|
+
runner.registerCheck(createOgTitleCheck())
|
|
60
|
+
runner.registerCheck(createOgDescriptionCheck())
|
|
61
|
+
runner.registerCheck(createOgImageCheck())
|
|
62
|
+
runner.registerCheck(createImageAltMissingCheck())
|
|
63
|
+
runner.registerCheck(createImageAltQualityCheck())
|
|
64
|
+
runner.registerCheck(createMetaDuplicateCheck())
|
|
65
|
+
runner.registerCheck(createViewportMissingCheck())
|
|
66
|
+
runner.registerCheck(createNoindexDetectedCheck())
|
|
67
|
+
runner.registerCheck(createTwitterCardCheck())
|
|
68
|
+
runner.registerSiteCheck(createRobotsTxtCheck())
|
|
69
|
+
runner.registerSiteCheck(createSitemapXmlCheck())
|
|
70
|
+
runner.registerSiteCheck(createBrokenInternalLinksCheck())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// GEO checks
|
|
74
|
+
if (options.geo !== false) {
|
|
75
|
+
const geo = options.geo
|
|
76
|
+
const minContent = geo.minContentLength ?? 300
|
|
77
|
+
const minHeadings = geo.minHeadings ?? 2
|
|
78
|
+
|
|
79
|
+
runner.registerSiteCheck(createLlmsTxtCheck())
|
|
80
|
+
runner.registerCheck(createContentTooShortCheck(minContent))
|
|
81
|
+
runner.registerCheck(createInsufficientHeadingsCheck(minHeadings))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Performance checks
|
|
85
|
+
if (options.performance !== false) {
|
|
86
|
+
const perf = options.performance
|
|
87
|
+
const maxHtml = perf.maxHtmlSize ?? 100_000
|
|
88
|
+
const maxImg = perf.maxImageSize ?? 500_000
|
|
89
|
+
const formats = perf.allowedImageFormats ?? ['webp', 'avif', 'svg']
|
|
90
|
+
const maxInline = perf.maxInlineSize ?? 50_000
|
|
91
|
+
const maxRequests = perf.maxExternalRequests ?? 20
|
|
92
|
+
|
|
93
|
+
runner.registerCheck(createHtmlSizeCheck(maxHtml))
|
|
94
|
+
runner.registerCheck(createImageFormatCheck(formats))
|
|
95
|
+
runner.registerCheck(createImageSizeCheck(maxImg))
|
|
96
|
+
runner.registerCheck(createLazyLoadingCheck())
|
|
97
|
+
runner.registerCheck(createRenderBlockingScriptCheck())
|
|
98
|
+
runner.registerCheck(createInlineSizeCheck(maxInline))
|
|
99
|
+
runner.registerCheck(createTotalRequestsCheck(maxRequests))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Accessibility checks
|
|
103
|
+
if (options.accessibility !== false) {
|
|
104
|
+
runner.registerCheck(createLangAttributeCheck())
|
|
105
|
+
runner.registerCheck(createFormLabelCheck())
|
|
106
|
+
runner.registerCheck(createAriaLandmarksCheck())
|
|
107
|
+
runner.registerCheck(createLinkTextCheck())
|
|
108
|
+
runner.registerCheck(createTabindexCheck())
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AstroIntegrationLogger } from 'astro'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { CheckReport, CheckResult } from './types'
|
|
5
|
+
|
|
6
|
+
export function logReport(report: CheckReport, logger: AstroIntegrationLogger): void {
|
|
7
|
+
const { totalPages, totalChecks, errors, warnings, infos } = report
|
|
8
|
+
|
|
9
|
+
if (errors.length === 0 && warnings.length === 0 && infos.length === 0) {
|
|
10
|
+
logger.info(`Checked ${totalPages} pages (${totalChecks} rules) — all passed`)
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
logger.info(`Checked ${totalPages} pages (${totalChecks} rules)`)
|
|
15
|
+
|
|
16
|
+
if (errors.length > 0) {
|
|
17
|
+
logger.error(`${errors.length} error(s):`)
|
|
18
|
+
for (const r of errors) {
|
|
19
|
+
logger.error(formatResult(r))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (warnings.length > 0) {
|
|
24
|
+
logger.warn(`${warnings.length} warning(s):`)
|
|
25
|
+
for (const r of warnings) {
|
|
26
|
+
logger.warn(formatResult(r))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (infos.length > 0) {
|
|
31
|
+
logger.info(`${infos.length} info(s):`)
|
|
32
|
+
for (const r of infos) {
|
|
33
|
+
logger.info(formatResult(r))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const parts = [
|
|
38
|
+
errors.length > 0 ? `${errors.length} error(s)` : null,
|
|
39
|
+
warnings.length > 0 ? `${warnings.length} warning(s)` : null,
|
|
40
|
+
infos.length > 0 ? `${infos.length} info(s)` : null,
|
|
41
|
+
].filter(Boolean)
|
|
42
|
+
|
|
43
|
+
logger.info(`Summary: ${parts.join(', ')}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function writeJsonReport(report: CheckReport, distDir: string, filename: string): Promise<string> {
|
|
47
|
+
const filePath = path.join(distDir, filename)
|
|
48
|
+
const json = {
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
totalPages: report.totalPages,
|
|
51
|
+
totalChecks: report.totalChecks,
|
|
52
|
+
passed: report.passed,
|
|
53
|
+
summary: {
|
|
54
|
+
errors: report.errors.length,
|
|
55
|
+
warnings: report.warnings.length,
|
|
56
|
+
infos: report.infos.length,
|
|
57
|
+
},
|
|
58
|
+
results: report.results.map(r => ({
|
|
59
|
+
checkId: r.checkId,
|
|
60
|
+
severity: r.severity,
|
|
61
|
+
domain: r.domain,
|
|
62
|
+
message: r.message,
|
|
63
|
+
suggestion: r.suggestion,
|
|
64
|
+
pagePath: r.pagePath,
|
|
65
|
+
line: r.line,
|
|
66
|
+
actual: r.actual,
|
|
67
|
+
expected: r.expected,
|
|
68
|
+
})),
|
|
69
|
+
}
|
|
70
|
+
await fs.writeFile(filePath, JSON.stringify(json, null, '\t'), 'utf8')
|
|
71
|
+
return filePath
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatResult(r: CheckResult): string {
|
|
75
|
+
const location = r.line ? `${r.pagePath}:${r.line}` : r.pagePath
|
|
76
|
+
const suggestion = r.suggestion ? ` → ${r.suggestion}` : ''
|
|
77
|
+
return ` ${r.checkId.padEnd(30)} ${location.padEnd(30)} ${r.message}${suggestion}`
|
|
78
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { HTMLElement as ParsedHTMLElement } from 'node-html-parser'
|
|
2
|
+
|
|
3
|
+
// ── Severity & Domain ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type CheckSeverity = 'error' | 'warning' | 'info'
|
|
6
|
+
export type CheckDomain = 'seo' | 'geo' | 'performance' | 'accessibility' | 'ai'
|
|
7
|
+
export type CheckMode = 'auto' | 'full' | 'essential'
|
|
8
|
+
|
|
9
|
+
// ── Check Issue (returned by checks) ────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Lightweight result returned by a check's run() method. The runner enriches it into a full CheckResult. */
|
|
12
|
+
export interface CheckIssue {
|
|
13
|
+
message: string
|
|
14
|
+
suggestion?: string
|
|
15
|
+
pagePath?: string
|
|
16
|
+
filePath?: string
|
|
17
|
+
line?: number
|
|
18
|
+
actual?: string
|
|
19
|
+
expected?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Issue returned by a site-level check — pagePath is required since there's no single page context. */
|
|
23
|
+
export type SiteCheckIssue = CheckIssue & { pagePath: string }
|
|
24
|
+
|
|
25
|
+
// ── Check Result (enriched by runner) ───────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface CheckResult {
|
|
28
|
+
checkId: string
|
|
29
|
+
ruleName: string
|
|
30
|
+
domain: CheckDomain
|
|
31
|
+
severity: CheckSeverity
|
|
32
|
+
message: string
|
|
33
|
+
suggestion?: string
|
|
34
|
+
pagePath: string
|
|
35
|
+
filePath?: string
|
|
36
|
+
line?: number
|
|
37
|
+
actual?: string
|
|
38
|
+
expected?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Check Interfaces ───────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** Per-page check — runs once for each HTML page */
|
|
44
|
+
export interface Check {
|
|
45
|
+
kind: 'page'
|
|
46
|
+
id: string
|
|
47
|
+
name: string
|
|
48
|
+
domain: CheckDomain
|
|
49
|
+
defaultSeverity: CheckSeverity
|
|
50
|
+
description: string
|
|
51
|
+
/** Whether this check runs in essential mode (true) or only in full mode (false) */
|
|
52
|
+
essential: boolean
|
|
53
|
+
run(context: PageCheckContext): CheckIssue[] | Promise<CheckIssue[]>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Site-level check — runs once after all pages are processed */
|
|
57
|
+
export interface SiteCheck {
|
|
58
|
+
kind: 'site'
|
|
59
|
+
id: string
|
|
60
|
+
name: string
|
|
61
|
+
domain: CheckDomain
|
|
62
|
+
defaultSeverity: CheckSeverity
|
|
63
|
+
description: string
|
|
64
|
+
essential: boolean
|
|
65
|
+
run(context: SiteCheckContext): SiteCheckIssue[] | Promise<SiteCheckIssue[]>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Check Contexts ─────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export interface PageCheckContext {
|
|
71
|
+
pagePath: string
|
|
72
|
+
filePath: string
|
|
73
|
+
distDir: string
|
|
74
|
+
html: string
|
|
75
|
+
root: ParsedHTMLElement
|
|
76
|
+
pageData: ExtractedPageData
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SiteCheckContext {
|
|
80
|
+
distDir: string
|
|
81
|
+
projectRoot: string
|
|
82
|
+
pages: Map<string, ExtractedPageData>
|
|
83
|
+
siteUrl?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Extracted Page Data ────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export interface ExtractedPageData {
|
|
89
|
+
title?: { content: string; line: number }
|
|
90
|
+
metaDescription?: { content: string; line: number }
|
|
91
|
+
metaTags: MetaTagData[]
|
|
92
|
+
openGraph: Record<string, { content: string; line: number }>
|
|
93
|
+
twitterCard: Record<string, { content: string; line: number }>
|
|
94
|
+
canonical?: { href: string; line: number }
|
|
95
|
+
jsonLd: JsonLdData[]
|
|
96
|
+
headings: HeadingData[]
|
|
97
|
+
images: ImageData[]
|
|
98
|
+
links: LinkData[]
|
|
99
|
+
scripts: ScriptData[]
|
|
100
|
+
stylesheets: StylesheetData[]
|
|
101
|
+
forms: ExtractedFormData[]
|
|
102
|
+
htmlLang?: string
|
|
103
|
+
htmlSize: number
|
|
104
|
+
bodyTextLength: number
|
|
105
|
+
hasViewport: boolean
|
|
106
|
+
hasNoindex: boolean
|
|
107
|
+
inlineScriptBytes: number
|
|
108
|
+
inlineStyleBytes: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface MetaTagData {
|
|
112
|
+
name?: string
|
|
113
|
+
property?: string
|
|
114
|
+
content: string
|
|
115
|
+
line: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface JsonLdData {
|
|
119
|
+
type: string
|
|
120
|
+
raw: string
|
|
121
|
+
valid: boolean
|
|
122
|
+
error?: string
|
|
123
|
+
line: number
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface HeadingData {
|
|
127
|
+
level: number
|
|
128
|
+
text: string
|
|
129
|
+
line: number
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ImageData {
|
|
133
|
+
src: string
|
|
134
|
+
alt?: string
|
|
135
|
+
loading?: string
|
|
136
|
+
line: number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface LinkData {
|
|
140
|
+
href: string
|
|
141
|
+
text: string
|
|
142
|
+
rel?: string
|
|
143
|
+
line: number
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ScriptData {
|
|
147
|
+
src?: string
|
|
148
|
+
type?: string
|
|
149
|
+
isAsync: boolean
|
|
150
|
+
isDefer: boolean
|
|
151
|
+
isInline: boolean
|
|
152
|
+
size: number
|
|
153
|
+
line: number
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface StylesheetData {
|
|
157
|
+
href: string
|
|
158
|
+
media?: string
|
|
159
|
+
line: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ExtractedFormData {
|
|
163
|
+
inputs: FormInputData[]
|
|
164
|
+
line: number
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface FormInputData {
|
|
168
|
+
type: string
|
|
169
|
+
name?: string
|
|
170
|
+
id?: string
|
|
171
|
+
hasLabel: boolean
|
|
172
|
+
line: number
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Configuration ──────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
export interface ChecksOptions {
|
|
178
|
+
mode?: CheckMode
|
|
179
|
+
seo?: boolean | SeoChecksConfig
|
|
180
|
+
geo?: boolean | GeoChecksConfig
|
|
181
|
+
performance?: boolean | PerformanceChecksConfig
|
|
182
|
+
accessibility?: boolean | AccessibilityChecksConfig
|
|
183
|
+
ai?: AiChecksConfig | false
|
|
184
|
+
failOnError?: boolean
|
|
185
|
+
failOnWarning?: boolean
|
|
186
|
+
overrides?: Record<string, CheckSeverity | false>
|
|
187
|
+
customChecks?: (Check | SiteCheck)[]
|
|
188
|
+
/** Write a JSON report to the dist dir. `true` writes `checks-report.json`, a string sets a custom filename. */
|
|
189
|
+
reportJson?: boolean | string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface SeoChecksConfig {
|
|
193
|
+
titleMaxLength?: number
|
|
194
|
+
descriptionMaxLength?: number
|
|
195
|
+
descriptionMinLength?: number
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface GeoChecksConfig {
|
|
199
|
+
minContentLength?: number
|
|
200
|
+
minHeadings?: number
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface PerformanceChecksConfig {
|
|
204
|
+
maxHtmlSize?: number
|
|
205
|
+
maxImageSize?: number
|
|
206
|
+
allowedImageFormats?: string[]
|
|
207
|
+
maxInlineSize?: number
|
|
208
|
+
maxExternalRequests?: number
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type AccessibilityChecksConfig = {}
|
|
212
|
+
|
|
213
|
+
export interface AiChecksConfig {
|
|
214
|
+
apiKey?: string
|
|
215
|
+
checks?: string[]
|
|
216
|
+
maxPages?: number
|
|
217
|
+
cache?: boolean
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface ResolvedChecksOptions {
|
|
221
|
+
mode: CheckMode
|
|
222
|
+
seo: SeoChecksConfig | false
|
|
223
|
+
geo: GeoChecksConfig | false
|
|
224
|
+
performance: PerformanceChecksConfig | false
|
|
225
|
+
accessibility: AccessibilityChecksConfig | false
|
|
226
|
+
ai: AiChecksConfig | false
|
|
227
|
+
failOnError: boolean
|
|
228
|
+
failOnWarning: boolean
|
|
229
|
+
overrides: Record<string, CheckSeverity | false>
|
|
230
|
+
customChecks: (Check | SiteCheck)[]
|
|
231
|
+
reportJson: string | false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Report ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
export interface CheckReport {
|
|
237
|
+
totalPages: number
|
|
238
|
+
totalChecks: number
|
|
239
|
+
results: CheckResult[]
|
|
240
|
+
errors: CheckResult[]
|
|
241
|
+
warnings: CheckResult[]
|
|
242
|
+
infos: CheckResult[]
|
|
243
|
+
passed: boolean
|
|
244
|
+
}
|