no-app-import.js
2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* 规则 D — app 之间禁止互相调用。
*
* `apps/*` 是可执行应用边界,不是可复用库。任何代码都不能 `import '@app/foo'`,
* 也不能用相对路径跨进其他 app(或从 package/tool 跨进任意 app)。共享能力必须下沉到
* `packages/*`,经 `@drama/<name>` 公共面导入。
*/
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 === '<input>' || filename === '<text>') 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)
}
},
}
},
}