no-app-import.js 2.95 KB
/**
 * 规则 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)
        }
      },
    }
  },
}