/** * 规则 A — 包间禁深导入(架构红线 §6.4)。 * * 任何 `@drama/*` 或 `@app/*` 的导入只能命中包公共面(裸包名或带 `package.json#exports` * 子路径条目)。禁止深入包内部源码,如 `@drama/llm/src/internal/x.js`。 * * 判定:导入说明符匹配 `@scope/name/` 即视为深导入并报错。仅识别 `@drama` / `@app` * 两个受控 scope,第三方 scope(如 `@koa/router`)不受此规则管辖。 */ import { readFileSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, join } from 'node:path' const CONTROLLED_SCOPES = new Set(['@drama', '@app']) /** * 读取受控包的 `package.json`。从被 lint 的文件出发解析包入口,再向上寻找 name 匹配的 * package.json。解析失败一律返回 null(回退到"按段数判深导入"的保守行为)。 * @param {string} pkg `@scope/name` * @param {string} fromFile 被 lint 文件的绝对路径 * @returns {Record|null} */ function readPackageJson(pkg, fromFile) { try { const require = createRequire(fromFile) let dir = dirname(require.resolve(pkg)) for (let depth = 0; depth < 8; depth += 1) { try { const json = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) if (json.name === pkg) return json } catch { // 继续向上 } const parent = dirname(dir) if (parent === dir) break dir = parent } } catch { // 解析不出包入口:回退保守判定。 } return null } /** * 子路径是否命中 `package.json#exports` 声明的公共面(含 `*` 通配)。 * @param {unknown} exports * @param {string} subpath 形如 `./vg-token-store` */ function subpathIsExported(exports, subpath) { if (!exports || typeof exports !== 'object') return false for (const key of Object.keys(exports)) { if (typeof key !== 'string' || !key.startsWith('./')) continue if (key === subpath) return true const star = key.indexOf('*') if (star >= 0) { const pre = key.slice(0, star) const post = key.slice(star + 1) if (subpath.startsWith(pre) && subpath.endsWith(post) && subpath.length >= pre.length + post.length) return true } } return false } /** * @param {string} source 导入说明符。 * @param {string} fromFile 被 lint 文件路径。 * @returns {boolean} 是否为受控 scope 下的深导入(且未被 exports 声明为公共面)。 */ function isControlledDeepImport(source, fromFile) { if (typeof source !== 'string' || source[0] !== '@') return false const parts = source.split('/') // 受控包公共面只有两段:@scope/name。三段及以上才可能是深导入。 if (parts.length <= 2) return false if (!CONTROLLED_SCOPES.has(parts[0])) return false // `package.json#exports` 声明的子路径是公共面,不算深导入。 const pkg = `${parts[0]}/${parts[1]}` const subpath = `.${source.slice(pkg.length)}` const packageJson = readPackageJson(pkg, fromFile) if (packageJson && subpathIsExported(packageJson.exports, subpath)) return false return true } /** @type {import('eslint').Rule.RuleModule} */ export const noDeepImport = { meta: { type: 'problem', docs: { description: '禁止深入 @drama/* 或 @app/* 包内部源码,只能从包的 index 公共面导入。', }, schema: [], messages: { deepImport: '禁止深导入 "{{source}}"。请从包公共面 "{{pkg}}" 导入(每个包唯一公共面是其 index.js)。', }, }, create(context) { const fromFile = context.filename || context.getFilename?.() || '' /** @param {string} source @param {import('estree').Node} node */ function check(source, node) { if (isControlledDeepImport(source, fromFile)) { const parts = source.split('/') context.report({ node, messageId: 'deepImport', data: { source, pkg: `${parts[0]}/${parts[1]}` }, }) } } 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) } }, } }, }