no-subdomain-deep-import.js
3.79 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
88
89
90
91
92
93
94
95
96
97
98
99
/**
* 规则 B — 包内子域间禁深导入(架构红线 §6.6)。
*
* 含多个子域目录的业务包(如 `@drama/bundle` 的 `drama/ script/ character/ scene/ bundle/`,
* 或 `@drama/assets` 的 `character/ scene/ prop/ effect/ sfx/ expression-pack/ repo/`)之间,
* 只能走各自子域的 `index.js`。把「禁深导入」递归进包内。
*
* 判定:当前文件位于包 `src/<subA>/...`,相对导入解析到 `src/<subB>/...`(subB ≠ subA)时,
* 只有命中 `src/<subB>/index.js`(或目录形式 `../<subB>` / `../<subB>/`)才放行;命中
* `src/<subB>/internal/...` 等更深路径即报错。同一子域内(subB === subA)相对导入不受限。
*
* 用 `path.posix` 归一化,避免平台分隔符差异。
*/
import path from 'node:path'
/**
* 取 `<root>/src/<subdomain>/...` 中的 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 === '<input>' || filename === '<text>') 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)
}
},
}
},
}