142 行
4.7 KiB
JavaScript
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
|
|
}
|