本地联调防火墙:用 Python 做 Monorepo 依赖自检

在大型项目或全栈开发中,Monorepo(单仓多包)架构越来越常见。为了在本地快速调试不同包之间的交互,开发者通常会在 package.json 里用 file: 协议声明本地路径依赖,比如 "core": "file:../core"。但这种做法有个隐患:如果引用的本地包没编译,或者代码里写了绝对路径,上线时可能直接报错。

一、本地联调的痛点

假设核心库 package A 被业务工程 package B 通过本地路径引用。当你在 package B 启动开发服务器时,打包工具会通过软链接读取 package A 的源码。常见问题有两个:

  1. 编译产物缺失
    如果 package A 只改了 TypeScript 源码,没执行 npm run build 生成 dist/ 目录,本地测试可能正常(因为直接读了源码),但线上构建会报错——因为构建系统拉取的是未生成的 dist/ 目录。

  2. 敏感文件泄露
    如果 package Apackage.json 没配置好 files 白名单,可能意外把 .env 密钥或 tests/ 目录打包进去,导致安全隐患。

核心问题:如何在发包前自动检查本地依赖的编译状态和文件白名单?

二、自检方案:静态分析本地依赖

我们设计了一个轻量级检查流程:

graph TD
    A[启动打包流水线] --> B[解析 package.json 依赖声明]
    B --> C[筛选 file:/link: 本地依赖]
    C --> D{检查路径是否存在?}
    D -- 否 --> E[报错:路径失效]
    D -- 是 --> F{检查入口文件是否存在?}
    F -- 否 --> G[报错:未编译]
    F -- 是 --> H{检查 files 白名单是否含敏感词?}
    H -- 含敏感词 --> I[报错:泄露风险]
    H -- 合规 --> J[允许发布]

这个流程能拦截两类问题:

  • 本地依赖未编译(dist/ 目录缺失)
  • 白名单配置错误(包含 .envtest/ 等敏感路径)

三、Python 实现:轻量级审计工具

下面是一个用 Python 原生模块实现的检查脚本,不依赖第三方库:

# local_dep_auditor.py
import json
import os
import sys

# 禁止包含的敏感关键词
FORBIDDEN = {".env", "secret", "credentials"}

def log(msg, error=False):
    prefix = "\033[31m[ERROR]\033[0m" if error else "\033[36m[Auditor]\033[0m"
    print(f"{prefix} {msg}")

class DependencyAuditor:
    def __init__(self, pkg_json_path):
        self.root = os.path.dirname(os.path.abspath(pkg_json_path))
    
    def audit(self):
        with open(self.root / "package.json") as f:
            pkg = json.load(f)
        
        errors = []
        for dep, version in {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}.items():
            if not version.startswith(("file:", "link:")):
                continue
            
            dep_path = os.path.normpath(os.path.join(self.root, version.split(":")[1]))
            
            # 检查路径存在性
            if not os.path.exists(dep_path):
                errors.append(f"❌ {dep}: 路径不存在 ({dep_path})")
                continue
            
            # 检查编译产物
            dep_pkg_path = dep_path / "package.json"
            if not os.path.exists(dep_pkg_path):
                errors.append(f"❌ {dep}: 缺少 package.json")
                continue
            
            with open(dep_pkg_path) as f:
                dep_pkg = json.load(f)
            
            main_file = dep_pkg.get("main", "")
            if main_file and not os.path.exists(dep_path / main_file):
                errors.append(f"❌ {dep}: 入口文件缺失 ({main_file}),请运行 build")
            
            # 检查白名单
            for pattern in dep_pkg.get("files", []):
                if any(kw in pattern.lower() for kw in FORBIDDEN):
                    errors.append(f"⚠️ {dep}: 白名单含敏感路径 ({pattern})")
        
        if errors:
            log("发现以下问题:", error=True)
            for e in errors:
                print(f"  {e}")
            return False
        
        log("✅ 所有本地依赖检查通过")
        return True

if __name__ == "__main__":
    auditor = DependencyAuditor("package.json")
    sys.exit(0 if auditor.audit() else 1)

关键逻辑

  1. 扫描 dependenciesdevDependencies 中的 file:/link: 依赖
  2. 检查对应路径是否存在
  3. 验证 main 字段指向的文件是否真实存在(即是否已编译)
  4. 检查 files 白名单是否包含敏感关键词

四、工程实践中的取舍

  1. file: vs npm link
    npm link 会创建全局软链,可能导致模块冲突(比如重复加载 React)。file: 协议更明确,推荐优先使用。

  2. CI/CD 中的路径替换
    本地 file:../lib 依赖不能直接部署,需在构建流程中替换为正式版本号。可以用 replace-in-file 等工具在打包前处理 package.json

  3. 缓存问题
    本地修改后,Vite/Webpack 可能因缓存未刷新。建议在 package.jsondev 脚本中加入缓存清理:

    "scripts": {
      "dev": "rimraf .vite_cache && vite"
    }
    

五、总结

Monorepo 本地联调需要平衡效率和安全性。通过在 CI 前加入这个轻量级检查脚本,可以:

  • 提前发现未编译的依赖
  • 防止敏感文件泄露
  • 减少线上构建失败的概率

工具本身只有 50 行代码,但能拦截大部分本地联调中的低级错误。建议集成到 precommitprepublish 钩子中,强制团队遵守依赖规范。

更多推荐