172 行
6.2 KiB
JavaScript
172 行
6.2 KiB
JavaScript
|
|
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|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 个公开模块`)
|