mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
chore: Новый черновик DRAFT, удалить старые docs-v
This commit is contained in:
149
scripts/check-site.mjs
Normal file
149
scripts/check-site.mjs
Normal file
@@ -0,0 +1,149 @@
|
||||
import { readFile, readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL('../', import.meta.url))
|
||||
const distRoot = path.join(repositoryRoot, 'site', '.vitepress', 'dist')
|
||||
const rulesSource = path.join(repositoryRoot, 'DRAFT', 'rules', 'level-1.md')
|
||||
const siteOrigin = 'https://site.test'
|
||||
const siteBase = '/slm-design/'
|
||||
|
||||
const expectedPages = [
|
||||
'404.html',
|
||||
'index.html',
|
||||
'level-1/index.html',
|
||||
'level-1/terminology.html',
|
||||
'level-1/layers.html',
|
||||
'level-1/dependencies.html',
|
||||
'level-1/modules.html',
|
||||
'level-1/groups.html',
|
||||
'level-1/segments.html',
|
||||
'level-1/components.html',
|
||||
'level-1/nested-modules.html',
|
||||
'level-1/lifecycle.html',
|
||||
'level-1/validation.html',
|
||||
'rules/index.html',
|
||||
'rules/level-1.html',
|
||||
].sort()
|
||||
|
||||
async function collectHtmlFiles(directory, prefix = '') {
|
||||
const entries = await readdir(directory, { withFileTypes: true })
|
||||
const files = []
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = path.posix.join(prefix, entry.name)
|
||||
const absolutePath = path.join(directory, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...await collectHtmlFiles(absolutePath, relativePath))
|
||||
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
||||
files.push(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
function pageUrl(relativePath) {
|
||||
if (relativePath === 'index.html') return `${siteOrigin}${siteBase}`
|
||||
if (relativePath.endsWith('/index.html')) {
|
||||
return `${siteOrigin}${siteBase}${relativePath.slice(0, -'index.html'.length)}`
|
||||
}
|
||||
return `${siteOrigin}${siteBase}${relativePath.slice(0, -'.html'.length)}`
|
||||
}
|
||||
|
||||
function htmlPathForUrl(url) {
|
||||
const route = decodeURIComponent(url.pathname.slice(siteBase.length))
|
||||
if (!route) return 'index.html'
|
||||
if (route.endsWith('/')) return `${route}index.html`
|
||||
return `${route}.html`
|
||||
}
|
||||
|
||||
const actualPages = (await collectHtmlFiles(distRoot)).sort()
|
||||
if (JSON.stringify(actualPages) !== JSON.stringify(expectedPages)) {
|
||||
throw new Error(
|
||||
`Published page set differs from allowlist.\nExpected: ${expectedPages.join(', ')}\nActual: ${actualPages.join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
const htmlByPage = new Map()
|
||||
for (const relativePath of actualPages) {
|
||||
htmlByPage.set(relativePath, await readFile(path.join(distRoot, relativePath), 'utf8'))
|
||||
}
|
||||
|
||||
for (const [relativePath, html] of htmlByPage) {
|
||||
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1])
|
||||
const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index)
|
||||
if (duplicateIds.length > 0) {
|
||||
throw new Error(`${relativePath} contains duplicate ids: ${[...new Set(duplicateIds)].join(', ')}`)
|
||||
}
|
||||
|
||||
for (const match of html.matchAll(/<a\b[^>]*\bhref="([^"]+)"/g)) {
|
||||
const target = new URL(match[1], pageUrl(relativePath))
|
||||
if (target.origin !== siteOrigin) continue
|
||||
if (!target.pathname.startsWith(siteBase)) {
|
||||
throw new Error(`${relativePath} links outside the configured base: ${match[1]}`)
|
||||
}
|
||||
|
||||
const targetPath = htmlPathForUrl(target)
|
||||
const targetHtml = htmlByPage.get(targetPath)
|
||||
if (!targetHtml) {
|
||||
throw new Error(`${relativePath} contains broken link ${match[1]}`)
|
||||
}
|
||||
|
||||
if (target.hash) {
|
||||
const anchor = decodeURIComponent(target.hash.slice(1))
|
||||
if (!targetHtml.includes(`id="${anchor}"`)) {
|
||||
throw new Error(`${relativePath} links to missing anchor ${match[1]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rulesMarkdown = await readFile(rulesSource, 'utf8')
|
||||
const ruleIds = [...rulesMarkdown.matchAll(/^### (SLM-L1-[A-Z_]+-[AR]\d{3})$/gm)]
|
||||
.map((match) => match[1])
|
||||
const rulesHtml = htmlByPage.get('rules/level-1.html')
|
||||
const searchChunksDirectory = path.join(distRoot, 'assets', 'chunks')
|
||||
const searchChunks = (await readdir(searchChunksDirectory))
|
||||
.filter((file) => file.startsWith('@localSearchIndex') && file.endsWith('.js'))
|
||||
|
||||
if (searchChunks.length !== 1) {
|
||||
throw new Error(`Expected one local search index, found ${searchChunks.length}`)
|
||||
}
|
||||
|
||||
const searchModuleUrl = `${pathToFileURL(path.join(searchChunksDirectory, searchChunks[0])).href}?t=${Date.now()}`
|
||||
const searchData = JSON.parse((await import(searchModuleUrl)).default)
|
||||
const searchUrls = new Set(Object.values(searchData.documentIds))
|
||||
|
||||
for (const ruleId of ruleIds) {
|
||||
const anchor = ruleId.toLowerCase()
|
||||
const expectedUrl = `${siteBase}rules/level-1#${anchor}`
|
||||
|
||||
if (!rulesHtml.includes(`id="${anchor}"`)) {
|
||||
throw new Error(`Published registry does not contain anchor ${anchor}`)
|
||||
}
|
||||
|
||||
if (!searchUrls.has(expectedUrl)) {
|
||||
throw new Error(`Local search index does not contain canonical record ${expectedUrl}`)
|
||||
}
|
||||
}
|
||||
|
||||
const levelOneHtml = htmlByPage.get('level-1/index.html')
|
||||
if (!levelOneHtml.includes('class="doc-set-header"')) {
|
||||
throw new Error('Level 1 pages do not render the document header')
|
||||
}
|
||||
|
||||
const notFoundHtml = htmlByPage.get('404.html')
|
||||
if (!notFoundHtml.includes('Страница не найдена')) {
|
||||
throw new Error('404 page is not localized')
|
||||
}
|
||||
|
||||
const sitemap = await readFile(path.join(distRoot, 'sitemap.xml'), 'utf8')
|
||||
for (const forbiddenRoute of ['/ru/', '/domains/', '/specification/']) {
|
||||
if (sitemap.includes(forbiddenRoute)) {
|
||||
throw new Error(`Sitemap contains archival or excluded route ${forbiddenRoute}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Site check passed: ${actualPages.length - 1} pages and ${ruleIds.length} searchable rules.`)
|
||||
Reference in New Issue
Block a user