/** * 规则 B — 包内子域间禁深导入(架构红线 §6.6)。 * * 含多个子域目录的业务包(如 `@drama/bundle` 的 `drama/ script/ character/ scene/ bundle/`, * 或 `@drama/assets` 的 `character/ scene/ prop/ effect/ sfx/ expression-pack/ repo/`)之间, * 只能走各自子域的 `index.js`。把「禁深导入」递归进包内。 * * 判定:当前文件位于包 `src//...`,相对导入解析到 `src//...`(subB ≠ subA)时, * 只有命中 `src//index.js`(或目录形式 `../` / `..//`)才放行;命中 * `src//internal/...` 等更深路径即报错。同一子域内(subB === subA)相对导入不受限。 * * 用 `path.posix` 归一化,避免平台分隔符差异。 */ import path from 'node:path' /** * 取 `/src//...` 中的 src 之后第一段(子域名)与是否恰好是该子域 index。 * @param {string} absFile 归一化为 posix 的绝对路径。 * @returns {{ subdomain: string, isIndex: boolean } | null} */ function subdomainOf(absFile) { const parts = absFile.split('/') const srcIdx = parts.lastIndexOf('src') if (srcIdx === -1 || srcIdx + 1 >= parts.length) return null const after = parts.slice(srcIdx + 1) // src 直属文件(如 src/index.js)不属于任何子域。 if (after.length < 2) return null const subdomain = after[0] const isIndex = after.length === 2 && after[1] === 'index.js' return { subdomain, isIndex } } /** @type {import('eslint').Rule.RuleModule} */ export const noSubdomainDeepImport = { meta: { type: 'problem', docs: { description: '包内子域之间只能从对方子域的 index.js 导入,禁止深入其 internal/ 等内部文件。', }, schema: [], messages: { subdomainDeep: '禁止跨子域深导入 "{{source}}"。子域 "{{from}}" 引用子域 "{{to}}" 时只能导入 "../{{to}}"(其 index.js),不能深入其内部文件。', }, }, create(context) { const filename = context.filename || context.getFilename() if (!filename || filename === '' || filename === '') return {} const absFile = filename.split(path.sep).join('/') const maybeHere = subdomainOf(absFile) if (!maybeHere) return {} const here = maybeHere const fileDir = path.posix.dirname(absFile) /** @param {string} source @param {import('estree').Node} node */ function check(source, node) { if (typeof source !== 'string') return // 只看相对导入;包名导入归规则 A。 if (source[0] !== '.') return const resolved = path.posix.normalize(path.posix.join(fileDir, source)) const target = subdomainOf(resolved) if (!target) return // 同一子域内随意;跨子域只允许命中对方 index。 if (target.subdomain === here.subdomain) return if (target.isIndex) return context.report({ node, messageId: 'subdomainDeep', data: { source, from: here.subdomain, to: target.subdomain }, }) } return { ImportDeclaration(node) { check(String(node.source.value), node.source) }, ExportNamedDeclaration(node) { if (node.source) check(String(node.source.value), node.source) }, ExportAllDeclaration(node) { if (node.source) check(String(node.source.value), node.source) }, ImportExpression(node) { if (node.source && node.source.type === 'Literal') { check(String(node.source.value), node.source) } }, CallExpression(node) { if (node.callee.type !== 'Identifier' || node.callee.name !== 'require') return const source = node.arguments[0] if (source && source.type === 'Literal') { check(String(source.value), source) } }, } }, }