/** * 规则 D — app 之间禁止互相调用。 * * `apps/*` 是可执行应用边界,不是可复用库。任何代码都不能 `import '@app/foo'`, * 也不能用相对路径跨进其他 app(或从 package/tool 跨进任意 app)。共享能力必须下沉到 * `packages/*`,经 `@drama/` 公共面导入。 */ import path from 'node:path' /** * @param {string} absPath * @returns {string|null} */ function appNameOf(absPath) { const parts = absPath.split('/') const idx = parts.lastIndexOf('apps') if (idx === -1 || idx + 1 >= parts.length) return null return parts[idx + 1] || null } /** @type {import('eslint').Rule.RuleModule} */ export const noAppImport = { meta: { type: 'problem', docs: { description: '禁止 import @app/* 或跨目录导入 apps/*;共享代码必须放 packages/*。', }, schema: [], messages: { appPackage: '禁止导入 "{{source}}"。apps/* 是可执行应用边界,不是公共库;共享能力请下沉到 packages/*。', crossApp: '禁止跨 app 导入 "{{source}}"。当前边界 "{{from}}" 不能调用 app "{{to}}";共享能力请下沉到 packages/*。', }, }, create(context) { const filename = context.filename || context.getFilename() if (!filename || filename === '' || filename === '') return {} const absFile = filename.split(path.sep).join('/') const fileDir = path.posix.dirname(absFile) const fromApp = appNameOf(absFile) /** @param {string} source @param {import('estree').Node} node */ function check(source, node) { if (typeof source !== 'string' || source === '') return if (source === '@app' || source.startsWith('@app/')) { context.report({ node, messageId: 'appPackage', data: { source } }) return } if (source[0] !== '.') return const resolved = path.posix.normalize(path.posix.join(fileDir, source)) const targetApp = appNameOf(resolved) if (!targetApp) return if (fromApp && fromApp === targetApp) return context.report({ node, messageId: 'crossApp', data: { source, from: fromApp || 'non-app', to: targetApp }, }) } 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) } }, } }, }