vue打包路径敏感解决;vue路径大小写引入检查与修复

问题:window下文件的路径的大小写是不敏感的,而linux下文件的路径名称需要区分大小写,在用linux打包文vue项目的时候会出现路径错误。要在linux 下完成打包,需修改文件中路径引用严格区分大小写。

自动校验和修复效果:

文章目录


一、问题

1、问题截图:

问题解释:在src/plugins/components.js文件内找不到xxxx/src/components/MyUpload/index.vue

因为实际的文件是xxxx/src/components/Myupload/index.vue

2、问题原因:

windows下路径大小写不敏感 ,如/page/index.js== Page/index.js,但是在Linux环境下大小写敏感

·/page!=/Page 它们表示两个不同的文件夹,所以在我们开发时应该遵守规范,严格按照大小写引入。

还有个一原因,写路由的时候,会省略index.vue文件,比如/page/login/index.vue 写成page/login,打包的时候也会报错.

二、检查大小写及修复脚本

知道路径大小写问题后,如果一个个文件去查找是非常麻烦且会有遗漏;此时可通过以下脚本自动查找和修复:这个脚本会扫描项目文件,找出不匹配的路径引用并提供修复选项。

1、创建路径大小写检查修复脚本

在项目的根目录下,创建 fix-path-case.js 文件:

脚本特点

  1. 全面扫描:支持 Vue、JS、TS 等文件类型
  2. 智能修复:自动找到正确的大小写路径
  3. 安全可靠 :默认只报告不修改,需明确指定 --fix
  4. 详细报告:显示文件名、行号、原路径和建议路径
  5. 忽略无关目录:自动跳过 node_modules 等目录
csharp 复制代码
#!/usr/bin/env node

// 检查文件大小写引入问题:
// node fix-path-case.js
// 自动修复:
// node fix-path-case.js --fix

import fs from "fs" // 文件系统模块,用于读写文件
import path from "path" // 路径处理模块
import { promisify } from "util"

const readdir = promisify(fs.readdir)
const stat = promisify(fs.stat)
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
// const exists = promisify(fs.exists);

class PathCaseChecker {
  constructor(options = {}) {
    this.rootDir = options.rootDir || process.cwd()
    this.srcDir = path.join(this.rootDir, "src")
    this.extensions = options.extensions || [".vue", ".js", ".ts", ".jsx", ".tsx"]
    this.ignoreDirs = options.ignoreDirs || ["node_modules", ".git", "dist", "build"]
    this.autoFix = options.autoFix || false
    this.issues = []

    // 存储所有文件的真实路径映射(小写 -> 实际大小写)
    this.filePathMap = new Map()
    this.aliasMap = {
      "@": this.srcDir,
      "~": this.srcDir
    }
  }

  // 递归收集所有文件的实际路径
  async collectAllFiles(dir, relativePath = "") {
    try {
      const files = await readdir(dir)

      for (const file of files) {
        const fullPath = path.join(dir, file)
        const fileRelativePath = path.join(relativePath, file)
        const fileStat = await stat(fullPath)

        if (fileStat.isDirectory()) {
          if (!this.ignoreDirs.includes(file)) {
            await this.collectAllFiles(fullPath, fileRelativePath)
          }
        } else {
          const ext = path.extname(file)
          if (this.extensions.includes(ext)) {
            // 存储小写路径到实际路径的映射
            const lowerPath = fileRelativePath.toLowerCase().replace(/\\/g, "/")
            this.filePathMap.set(lowerPath, fileRelativePath.replace(/\\/g, "/"))
          }
        }
      }
    } catch (error) {
      console.error(`收集文件失败: ${dir}`, error.message)
    }
  }

  // 解析别名路径为实际路径
  resolveAliasPath(importPath) {
    if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
      const alias = importPath.startsWith("@/") ? "@" : "~"
      const relativePath = importPath.slice(2) // 去掉 '@/' 或 '~/'
      return path.join(this.aliasMap[alias], relativePath)
    }
    return importPath
  }

  // 检查导入路径是否存在大小写问题
  async checkImportPath(importPath, currentFile) {
    // 跳过非项目路径
    if (!importPath.startsWith("@/") && !importPath.startsWith("~/") && !importPath.startsWith(".")) {
      return null
    }

    let actualFilePath = importPath

    // 处理别名路径
    if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
      actualFilePath = this.resolveAliasPath(importPath)
    } else if (importPath.startsWith(".")) {
      // 处理相对路径
      const currentDir = path.dirname(currentFile)
      actualFilePath = path.resolve(currentDir, importPath)
    }

    // 转换为相对于 src 目录的路径
    let relativeToSrc = path.relative(this.srcDir, actualFilePath)
    if (relativeToSrc.startsWith("..")) {
      // 如果不在 src 目录内,使用相对于项目根目录的路径
      relativeToSrc = path.relative(this.rootDir, actualFilePath)
    }

    const normalizedRelativePath = relativeToSrc.replace(/\\/g, "/")
    const lowerPath = normalizedRelativePath.toLowerCase()

    // 检查是否存在大小写不匹配
    const actualCasePath = this.filePathMap.get(lowerPath)
    if (actualCasePath && actualCasePath !== normalizedRelativePath) {
      // 计算正确的导入路径
      let correctImportPath
      if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
        const alias = importPath.startsWith("@/") ? "@" : "~"
        correctImportPath = `${alias}/${actualCasePath}`
      } else {
        // 相对路径需要重新计算
        const currentDir = path.dirname(currentFile)
        const absoluteCorrectPath = path.join(this.srcDir, actualCasePath)
        correctImportPath = path.relative(currentDir, absoluteCorrectPath).replace(/\\/g, "/")
        if (!correctImportPath.startsWith(".")) {
          correctImportPath = "./" + correctImportPath
        }
      }

      return {
        original: importPath,
        correct: correctImportPath,
        actualFile: path.join(this.srcDir, actualCasePath)
      }
    }

    return null
  }

  // 提取文件中的所有导入语句(改进版)
  extractImports(content, filePath) {
    const imports = []

    // 匹配各种导入模式
    const patterns = [
      // import ... from 'path'
      /from\s+['"]([^'"]+)['"]/g,
      // import 'path'
      /import\s+['"]([^'"]+)['"]/g,
      // require('path')
      /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
      // 动态 import()
      /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g
    ]

    patterns.forEach(pattern => {
      let match
      while ((match = pattern.exec(content)) !== null) {
        const importPath = match[1]

        // 过滤掉明显不是文件导入的路径
        if (this.isFileImport(importPath, filePath)) {
          imports.push({
            path: importPath,
            start: match.index,
            end: match.index + match[0].length,
            original: match[0]
          })
        }
      }
    })

    return imports
  }

  // 判断是否为文件导入路径
  isFileImport(importPath, currentFile) {
    // 跳过明显的包导入
    if (!importPath.startsWith(".") && !importPath.startsWith("@/") && !importPath.startsWith("~/")) {
      return false
    }

    // 检查是否有文件扩展名或看起来像文件路径
    const ext = path.extname(importPath)
    if (ext && this.extensions.includes(ext)) {
      return true
    }

    // 对于目录导入,检查是否存在对应的文件
    return this.looksLikeFilePath(importPath, currentFile)
  }

  // 判断路径是否像文件路径
  looksLikeFilePath(importPath, currentFile) {
    // 包含目录分隔符或常见目录名
    if (importPath.includes("/") && !importPath.includes("node_modules")) {
      // 尝试解析路径并检查是否存在对应文件
      let resolvedPath
      if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
        resolvedPath = this.resolveAliasPath(importPath)
      } else {
        resolvedPath = path.resolve(path.dirname(currentFile), importPath)
      }

      // 检查是否存在对应的文件(任何扩展名)
      return this.extensions.some(ext => {
        const filePath = resolvedPath + ext
        return (
          fs.existsSync(filePath) || fs.existsSync(path.join(resolvedPath, "index" + ext)) || fs.existsSync(path.join(resolvedPath, "index.js")) || fs.existsSync(path.join(resolvedPath, "index.vue"))
        )
      })
    }

    return false
  }

  // 检查单个文件
  async checkFile(filePath) {
    try {
      const content = await readFile(filePath, "utf8")
      const imports = this.extractImports(content, filePath)
      let newContent = content
      let hasChanges = false
      const replacements = []

      // 收集所有需要替换的位置
      for (const imp of imports) {
        const result = await this.checkImportPath(imp.path, filePath)
        if (result) {
          replacements.push({
            original: imp.original,
            replacement: imp.original.replace(imp.path, result.correct),
            start: imp.start,
            end: imp.end,
            issue: {
              file: filePath,
              originalPath: imp.path,
              correctPath: result.correct,
              actualFile: result.actualFile
            }
          })
        }
      }

      // 从后往前替换,避免位置偏移
      replacements.sort((a, b) => b.start - a.start)

      for (const replacement of replacements) {
        const before = newContent.substring(0, replacement.start)
        const after = newContent.substring(replacement.end)
        newContent = before + replacement.replacement + after

        this.issues.push(replacement.issue)
        hasChanges = true
      }

      if (hasChanges && this.autoFix) {
        await writeFile(filePath, newContent, "utf8")
        console.log(`✅ 已修复: ${filePath}`)
      }

      return hasChanges
    } catch (error) {
      console.error(`❌ 检查文件失败: ${filePath}`, error.message)
      return false
    }
  }

  // 递归扫描目录
  async scanDirectory(dirPath) {
    try {
      const files = await readdir(dirPath)

      for (const file of files) {
        const fullPath = path.join(dirPath, file)
        const fileStat = await stat(fullPath)

        if (fileStat.isDirectory()) {
          if (!this.ignoreDirs.includes(file)) {
            await this.scanDirectory(fullPath)
          }
        } else {
          const ext = path.extname(file)
          if (this.extensions.includes(ext)) {
            await this.checkFile(fullPath)
          }
        }
      }
    } catch (error) {
      console.error(`❌ 扫描目录失败: ${dirPath}`, error.message)
    }
  }

  // 生成详细报告
  generateReport() {
    if (this.issues.length === 0) {
      console.log("\n🎉 没有发现路径大小写问题!")
      return
    }

    console.log(`\n📊 发现 ${this.issues.length} 个路径大小写问题,统计如下:\n`)

    const groupedIssues = this.issues.reduce((groups, issue) => {
      const key = issue.file
      if (!groups[key]) groups[key] = []
      groups[key].push(issue)
      return groups
    }, {})

    Object.entries(groupedIssues).forEach(([file, issues]) => {
      console.log(`📁 ${file}:`)
      issues.forEach((issue, index) => {
        console.log(`   ${index + 1}. 引入路径: ${issue.originalPath}`)
        console.log(`      正确路径: ${issue.correctPath}`)
        console.log(`      实际文件: ${issue.actualFile}`)
      })
      console.log("")
    })

    if (!this.autoFix) {
      console.log(`\n   ❌存在 ${this.issues.length} 个路径大小写问题:`)
      console.log('   💡建议运行 "node fix-path-case.js --fix" 自动修复这些问题')
    }
    if (this.autoFix && this.issues.length > 0) {
      console.log(`\n   ✅已修复路径大小写问题`)
    }
  }

  // 主执行函数
  async run() {
    console.log("🔍 开始收集项目文件信息...")
    await this.collectAllFiles(this.srcDir)

    console.log("📁 扫描路径大小写问题...")
    console.log(`项目根目录: ${this.rootDir}`)
    console.log(`源码目录: ${this.srcDir}`)

    await this.scanDirectory(this.srcDir)
    this.generateReport()

    if (this.issues.length > 0 && !this.autoFix) {
      // console.log('\n❌ 发现路径大小写问题,请运行修复命令');
      // process.exit(1) // 如果有问题且未修复,退出码为1
    }
  }
}

// CLI 参数解析
const args = process.argv.slice(2)
const options = {
  autoFix: args.includes("--fix") || args.includes("-f"),
  rootDir: process.cwd()
}

// 处理自定义目录
const dirIndex = args.findIndex(arg => arg === "--dir" || arg === "-d")
if (dirIndex !== -1 && args[dirIndex + 1]) {
  options.rootDir = path.resolve(args[dirIndex + 1])
}

// 运行检查器
const checker = new PathCaseChecker(options)
checker.run().catch(error => {
  console.error("❌ 执行失败:", error)
  process.exit(1)
})

2、使用说明

2.1、 基本使用

bash 复制代码
# 将脚本放在项目根目录,然后运行:
node fix-path-case.js

这会扫描项目并报告所有路径大小写问题,但不会自动修改。

2.2、自动修复

bash 复制代码
# 自动修复所有发现的问题
node fix-path-case.js --fix

2.3、 添加执行脚本到 package.json

package.json 中添加脚本:

json 复制代码
{
  "scripts": {
    "dev": "pnpm fix:paths && vite --mode beta --host",
    "lint:paths": "node fix-path-case.js",
    "fix:paths": "node fix-path-case.js --fix"
  }
}

然后运行:

bash 复制代码
pnpm lint:paths    # 检查问题
pnpm fix:paths     # 自动修复
pnpm dev  #启动项目时候自动校验和修复

pnpm lint:paths检查问题效果截图:

pnpm fix:paths自动修复效果截图:

pnpm dev启动项目时候自动校验和修复:

2.3、预防性配置

在 vite.config.js 中添加路径别名检查

javascript 复制代码
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      // 确保所有别名都使用正确的大小写
    },
  },
});

3、 其他配置(暂未验证,可以不配置)

3.1 配套的 ESLint 配置

创建 .eslintrc-path-case.js 文件来增强路径检查:

javascript 复制代码
module.exports = {
  rules: {
    'import/no-unresolved': ['error', { caseSensitive: true }],
  },
  overrides: [
    {
      files: ['*.vue'],
      rules: {
        'vue/component-name-in-template-casing': ['error', 'PascalCase'],
      }
    }
  ]
};

3.2 创建路径检查的 Git Hook

.husky/pre-commit 中添加:

bash 复制代码
#!/bin/bash
node fix-path-case.js --dry-run
if [ $? -ne 0 ]; then
  echo "发现路径大小写问题,请运行 'npm run fix:paths' 修复"
  exit 1
fi

三、配置git大小写敏感

3.1、检查git是否校验大小写敏感

git在提交代码时,会忽略文件名称大小写,导致本地代码与远程代码不一致,此时可利用终端指令来检查下

如果返回值为true,则表示Git会将文件名大小写视为不敏感 ;如果返回值为false ,则表示Git会将文件名大小写视为敏感

csharp 复制代码
# 查看全局设置
git config --get core.ignorecase
 
# 查看特定项目设置(需要进入项目根目录)
git config --global --get core.ignorecase

3.2配置大小写敏感

csharp 复制代码
# 设置全局设置
git config --global core.ignorecase false
 
# 设置特定项目设置(需要进入项目根目录)
git config core.ignorecase false
相关推荐
知了一笑7 小时前
项目效率翻倍,做对了什么?
前端·人工智能·后端
江城开朗的豌豆7 小时前
webpack了解吗,讲一讲原理,怎么压缩代码
前端·javascript·微信小程序
_xaboy7 小时前
开源设计器 FcDesigner 限制组件是否可以拖入的教程
前端·vue.js·低代码·开源·表单设计器
江城开朗的豌豆7 小时前
Webpack配置魔法书:从入门到高手的通关秘籍
前端·javascript·微信小程序
AnalogElectronic7 小时前
vue3 实现记事本手机版01
开发语言·javascript·ecmascript
江城开朗的豌豆7 小时前
玩转小程序生命周期:从入门到上瘾
前端·javascript·微信小程序
Fanfffff7207 小时前
从TSX到JS:深入解析npm run build背后的完整构建流程
开发语言·javascript·npm
im_AMBER7 小时前
React 10
前端·javascript·笔记·学习·react.js·前端框架
Moment7 小时前
记录一次修改 PNPM 版本,部署 NextJs 服务时导致服务器崩溃的问题 😡😡😡
前端·javascript·后端