XuqmGroup-Web/docs-site/scripts/check-rn-docs.mjs
2026-07-29 02:49:24 +08:00

175 行
6.2 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const docsSiteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const rnDocsRoot = path.join(docsSiteRoot, 'docs', 'rn')
const vitepressConfig = path.join(docsSiteRoot, 'docs', '.vitepress', 'config.ts')
const requiredPages = [
'index.md',
'setup.md',
'common.md',
'session.md',
'update.md',
'bugcollect.md',
'xwebview.md',
'im.md',
'push.md',
'troubleshooting.md',
]
const forbiddenPatterns = [
[/@xuqm\/rn-sdk\b/g, '内部聚合包不应进入对外文档'],
[/\bXuqmSDK\.initialize\s*\(/g, '已删除的手动初始化 API'],
[/\bcheckRnUpdate\s*\(/g, '已删除的旧 Update API'],
[/\bdownloadAndApplyBundle\s*\(/g, '已删除的旧 Bundle API'],
[/\brequestNativeRegistration\s*\(/g, '已删除的 RN Push Token API'],
[/\bregisterToken\s*\(/g, '已删除的 RN Push Token API'],
[/\bsetDeviceToken\s*\(/g, '已删除的 RN Push Token API'],
[/@xuqm\/rn-common\/internal(?:-security)?\b/g, '官方扩展专用内部入口'],
[
/\b(?:release[- ]set|Ed25519|NativeBundle|loadNativeBundle|planReleaseSet)\b/gi,
'内部更新实现细节',
],
[/(?:原生基线|签名算法|数据库隔离规则|服务拓扑)/g, '内部架构细节'],
]
const requiredPackagePages = new Map([
['@xuqm/rn-common', ['index.md', 'setup.md', 'common.md', 'session.md']],
['@xuqm/rn-update', ['update.md']],
['@xuqm/rn-bugcollect', ['bugcollect.md']],
['@xuqm/rn-xwebview', ['xwebview.md']],
['@xuqm/rn-im', ['im.md']],
['@xuqm/rn-push', ['push.md']],
])
const failures = []
const sourceByPage = new Map()
for (const page of requiredPages) {
const file = path.join(rnDocsRoot, page)
if (!fs.existsSync(file)) {
failures.push(`缺少 RN 对外文档页面:${page}`)
continue
}
sourceByPage.set(page, fs.readFileSync(file, 'utf8'))
}
const actualMarkdownPages = fs
.readdirSync(rnDocsRoot, { withFileTypes: true })
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
.map(entry => entry.name)
.sort()
for (const page of actualMarkdownPages) {
if (!requiredPages.includes(page)) {
failures.push(`存在未纳入公开信息架构的 RN 页面:${page}`)
}
}
for (const [page, source] of sourceByPage) {
for (const [pattern, reason] of forbiddenPatterns) {
pattern.lastIndex = 0
if (pattern.test(source)) {
failures.push(`${page} 包含禁止内容(${reason}${pattern}`)
}
}
}
for (const [packageName, pages] of requiredPackagePages) {
for (const page of pages) {
if (!sourceByPage.get(page)?.includes(packageName)) {
failures.push(`${page} 未明确记录公开包 ${packageName}`)
}
}
}
const navigation = fs.readFileSync(vitepressConfig, 'utf8')
for (const page of requiredPages) {
const route = page === 'index.md' ? "/rn/'" : `/rn/${page.replace(/\.md$/, '')}'`
if (!navigation.includes(route)) {
failures.push(`VitePress RN 导航缺少 ${page}`)
}
}
if (navigation.includes('/rn/group')) {
failures.push('VitePress RN 导航仍引用已经合并的重复群聊页面')
}
// 本地多仓工作区存在 RN SDK 源码时,继续核对文档中的命名导入确实由 package index 导出。
// Jenkins 单仓构建没有兄弟仓库时跳过此增强检查,但上面的静态契约仍然生效。
const sdkRoot =
process.env.XUQM_RN_SDK_DIR ??
path.resolve(docsSiteRoot, '..', '..', 'XuqmGroup-RNSDK')
if (fs.existsSync(path.join(sdkRoot, 'packages'))) {
const indexByPackage = new Map()
for (const packageDir of ['common', 'update', 'bugcollect', 'xwebview', 'im', 'push']) {
const packageJson = JSON.parse(
fs.readFileSync(path.join(sdkRoot, 'packages', packageDir, 'package.json'), 'utf8'),
)
indexByPackage.set(
packageJson.name,
fs.readFileSync(path.join(sdkRoot, 'packages', packageDir, 'src', 'index.ts'), 'utf8'),
)
}
const namedImport =
/import\s*\{([\s\S]*?)\}\s*from\s*['"](@xuqm\/rn-(?:common|update|bugcollect|xwebview|im|push))['"]/g
for (const [page, source] of sourceByPage) {
for (const match of source.matchAll(namedImport)) {
const packageSource = indexByPackage.get(match[2])
const symbols = match[1]
.split(',')
.map(value => value.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0])
.filter(value => /^[A-Za-z_$][\w$]*$/.test(value))
for (const symbol of symbols) {
const exported = new RegExp(
`(?:export\\s+(?:const|class|function|type|interface)\\s+${symbol}\\b|export\\s*\\{[\\s\\S]*?\\b${symbol}\\b[\\s\\S]*?\\})`,
).test(packageSource)
if (!exported) {
failures.push(`${page}${match[2]} 导入未公开的符号:${symbol}`)
}
}
}
}
const objectSources = new Map([
['XuqmSDK', path.join(sdkRoot, 'packages', 'common', 'src', 'sdk.ts')],
['UpdateSDK', path.join(sdkRoot, 'packages', 'update', 'src', 'UpdateSDK.ts')],
[
'NativeAppUpdate',
path.join(sdkRoot, 'packages', 'update', 'src', 'NativeAppUpdate.ts'),
],
['XuqmRuntime', path.join(sdkRoot, 'packages', 'update', 'src', 'PluginRuntime.ts')],
['BugCollect', path.join(sdkRoot, 'packages', 'bugcollect', 'src', 'BugCollect.ts')],
['ImSDK', path.join(sdkRoot, 'packages', 'im', 'src', 'ImSDK.ts')],
['PushSDK', path.join(sdkRoot, 'packages', 'push', 'src', 'PushSDK.ts')],
])
for (const [objectName, sourceFile] of objectSources) {
const implementation = fs.readFileSync(sourceFile, 'utf8')
const usage = new RegExp(`\\b${objectName}\\.([A-Za-z_$][\\w$]*)\\s*\\(`, 'g')
for (const [page, source] of sourceByPage) {
for (const match of source.matchAll(usage)) {
const method = match[1]
const declared = new RegExp(
`(?:^|\\n)\\s*(?:async\\s+)?${method}\\s*(?:\\([^\\n]*|:)`,
).test(implementation)
if (!declared) {
failures.push(`${page} 调用 ${objectName} 中不存在的公开方法:${method}`)
}
}
}
}
}
if (failures.length > 0) {
console.error('RN 对外文档契约检查失败:')
for (const failure of failures) console.error(`- ${failure}`)
process.exit(1)
}
console.log(`RN 对外文档契约检查通过:${requiredPages.length} 个页面,6 个公开模块`)