XuqmGroup-RNSDK/packages/update/scripts/source-boundaries.mjs

142 行
4.7 KiB
JavaScript

import { existsSync, readFileSync, readdirSync } from 'node:fs'
import path from 'node:path'
const SOURCE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
function inside(file, directory) {
const relative = path.relative(directory, file)
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
}
function walk(directory) {
if (!existsSync(directory)) return []
return readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
const absolute = path.join(directory, entry.name)
if (entry.isDirectory()) return walk(absolute)
return SOURCE_EXTENSIONS.includes(path.extname(entry.name)) ? [absolute] : []
})
}
function sourceSpecifiers(file) {
const source = readFileSync(file, 'utf8')
const result = []
for (const pattern of [
/(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g,
/\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
]) {
for (const match of source.matchAll(pattern)) result.push(match[1])
}
return result
}
function aliasMappings(projectRoot) {
const conventional = [
{ alias: '@app', root: path.join(projectRoot, 'src', 'app') },
{ alias: '@shared', root: path.join(projectRoot, 'src', 'shared') },
{ alias: '@plugins', root: path.join(projectRoot, 'src', 'plugins') },
]
const tsconfigFile = path.join(projectRoot, 'tsconfig.json')
if (!existsSync(tsconfigFile)) return conventional
let config
try {
config = JSON.parse(readFileSync(tsconfigFile, 'utf8'))
} catch {
// 带注释的 tsconfig 仍使用稳定约定别名;doctor 不应为了静态门禁要求宿主改格式。
return conventional
}
const paths = config.compilerOptions?.paths ?? {}
return [
...conventional,
...Object.entries(paths).flatMap(([pattern, targets]) => {
const alias = pattern.replace(/\/\*$/, '')
return (targets ?? []).map(target => ({
alias,
root: path.resolve(projectRoot, target.replace(/\/\*$/, '')),
}))
}),
]
}
function resolveSource(importer, specifier, aliases) {
let base
if (specifier.startsWith('.')) {
base = path.resolve(path.dirname(importer), specifier)
} else {
const mapping = aliases
.sort((left, right) => right.alias.length - left.alias.length)
.find(item => specifier === item.alias || specifier.startsWith(`${item.alias}/`))
if (!mapping) return null
const suffix = specifier === mapping.alias ? '' : specifier.slice(mapping.alias.length + 1)
base = path.join(mapping.root, suffix)
}
return [
base,
...SOURCE_EXTENSIONS.map(extension => `${base}${extension}`),
...SOURCE_EXTENSIONS.map(extension => path.join(base, `index${extension}`)),
].find(existsSync)
}
/**
* 这些是所有多 Bundle 宿主都必须满足的交付边界不属于任何具体 App 的业务规则
*/
export function validateSourceBoundaries(projectRoot, config) {
if (!config.entryGeneration) return []
const aliases = aliasMappings(projectRoot)
const appRoot = path.dirname(path.resolve(projectRoot, config.entryGeneration.appComponent))
const commonRoots = (
config.modules.find(module => module.type === 'common')?.ownershipRoots ?? []
).map(root => path.resolve(projectRoot, root))
const pluginRoots = config.modules
.filter(module => module.type === 'buz')
.map(module => ({ id: module.id, root: path.dirname(path.resolve(projectRoot, module.entry)) }))
const violations = []
function inspect(importer, owner, isForbidden) {
for (const specifier of sourceSpecifiers(importer)) {
const dependency = resolveSource(importer, specifier, aliases)
if (!dependency || !isForbidden(dependency)) continue
violations.push(
`${owner}: ${path.relative(projectRoot, importer)} -> ${path.relative(
projectRoot,
dependency,
)}`,
)
}
}
for (const commonRoot of commonRoots) {
for (const importer of walk(commonRoot)) {
inspect(
importer,
'common 不得依赖宿主或插件',
dependency =>
inside(dependency, appRoot) ||
pluginRoots.some(plugin => inside(dependency, plugin.root)),
)
}
}
for (const plugin of pluginRoots) {
for (const importer of walk(plugin.root)) {
inspect(
importer,
`插件 ${plugin.id} 只能依赖自身、common 和三方库`,
dependency =>
inside(dependency, appRoot) ||
pluginRoots.some(
candidate => candidate.id !== plugin.id && inside(dependency, candidate.root),
),
)
}
}
for (const importer of walk(appRoot)) {
inspect(importer, '宿主不得静态导入插件', dependency =>
pluginRoots.some(plugin => inside(dependency, plugin.root)),
)
}
return violations
}