babel: rewrite-import-cli 重写vue-router中的import

本文在上一篇内容上进行进一步改造,包装成一个随处可用的 cli

背景

webpack 提供了 require的方式导入模块, 但是我们希望统一改为 import的写法

javascript 复制代码
component: (resolve) => require(['@/views/xxxx/retrieveCaptcha/index.vue'], resolve),

希望改成

javascript 复制代码
 component: () => import('@/views/xxxx/retrieveCaptcha/index.vue'),

项目越来越大的时候,修改比较吃力, 如果是webpack 迁移到 vite项目工程,迁移难度也比较大

所以,写了这样一个工具,一键转换

功能

  • 转换 require 为 import
  • 转换后的代码,复用项目中的 prettier配置,对代码做格式化处理(如果不存在,则忽略)

安装

建议全局安装使用

arduino 复制代码
npm i rewrite-import-cli -g

使用

shell 复制代码
re-import

过程

lua 复制代码
PS D:\projects\os> re-import
? 项目运行根目录:  D:\projects\os Yes
? 路由路径 src/router

路由路径,输入一个要处理的 路径,可以是一个文件

  1. 有的项目,所有的 路由都放在一个文件, 可以指定文件
  2. 有的项目分了模块,拆分了不同的js文件存放路由, 可以直接指定父目录

案例整个运行日志输出:

lua 复制代码
PS D:\projects\os> re-import
? 项目运行根目录:  D:\projects\os Yes
? 路由路径 src/router
拿到的所有路由文件: 【16】个
[
  'D:\\projects\\os\\src\\router\\index.js',
  'D:\\projects\\os\\src\\router\\modules\\workbench.js',
  'D:\\projects\\os\\src\\router\\modules\\technology.js',
  'D:\\projects\\os\\src\\router\\modules\\systemManage.js',
  'D:\\projects\\os\\src\\router\\modules\\subSystem.js',
  'D:\\projects\\os\\src\\router\\modules\\personalComplaint.js',
  'D:\\projects\\os\\src\\router\\modules\\personalCenter.js',
  'D:\\projects\\os\\src\\router\\modules\\home.js',
  'D:\\projects\\os\\src\\router\\modules\\handleError.js',
  'D:\\projects\\os\\src\\router\\modules\\dogcart.js',
  'D:\\projects\\os\\src\\router\\modules\\dataServer.js',
  'D:\\projects\\os\\src\\router\\modules\\chatMessages.js',
  'D:\\projects\\os\\src\\router\\modules\\businessSystem.js',
  'D:\\projects\\os\\src\\router\\modules\\baoganManage.js',
  'D:\\projects\\os\\src\\router\\modules\\assetInventory.js',
  'D:\\projects\\os\\src\\router\\modules\\addressList.js'
]
项目的配置文件对象
{
  printWidth: 200,
  tabWidth: 2,
  useTabs: false,
  semi: false,
  singleQuote: true,
  quoteProps: 'as-needed',
  jsxSingleQuote: false,
  trailingComma: 'none',
  bracketSpacing: true,
  jsxBracketSameLine: false,
  arrowParens: 'avoid',
  htmlWhitespaceSensitivity: 'ignore',
  vueIndentScriptAndStyle: false,
  endOfLine: 'auto'
}
转换完成
有require的文件清单 【12】个
[
  'D:\\projects\\os\\src\\router\\modules\\workbench.js',
  'D:\\projects\\os\\src\\router\\modules\\technology.js',
  'D:\\projects\\os\\src\\router\\modules\\systemManage.js',
  'D:\\projects\\os\\src\\router\\modules\\personalComplaint.js',
  'D:\\projects\\os\\src\\router\\modules\\personalCenter.js',
  'D:\\projects\\os\\src\\router\\modules\\home.js',
  'D:\\projects\\os\\src\\router\\modules\\handleError.js',
  'D:\\projects\\os\\src\\router\\modules\\dogcart.js',
  'D:\\projects\\os\\src\\router\\modules\\dataServer.js',
  'D:\\projects\\os\\src\\router\\modules\\chatMessages.js',
  'D:\\projects\\os\\src\\router\\modules\\businessSystem.js',
  'D:\\projects\\os\\src\\router\\modules\\addressList.js'
]

代码实现

csharp 复制代码
pnpm init
shell 复制代码
pnpm add glob cac inquirer prettier -S
  • cac 交互式终端 跟 commander 作用差不多
  • glob 比较方便的拿到目录里面的文件

babel相关

shell 复制代码
pnpm add @babel/generator @babel/parser @babel/traverse @babel/types -S

使用ts开发, 类型提示相关依赖

sql 复制代码
pnpm add @types/babel-types  @types/babel__generator @types/babel__traverse @types/inquirer -D

pnpm add @types/node ts-node tslib typescript -D

项目依赖版本号

基于 node: 20.10.0 开发

node 20.10.0
json 复制代码
 "dependencies": {
    "@babel/generator": "^7.23.6",
    "@babel/parser": "^7.23.9",
    "@babel/traverse": "^7.23.9",
    "@babel/types": "^7.23.9",
    "cac": "^6.7.14",
    "commander": "^12.0.0",
    "glob": "^10.3.10",
    "inquirer": "^9.2.15",
    "prettier": "^3.2.5"
  },
  "devDependencies": {
    "@types/babel-types": "^7.0.15",
    "@types/babel__generator": "^7.6.8",
    "@types/babel__traverse": "^7.20.5",
    "@types/inquirer": "^9.0.7",
    "@types/node": "^20.11.20",
    "ts-node": "^10.9.2",
    "tslib": "^2.6.2",
    "typescript": "^5.3.3"
  }

代码

初始化 tsconfig.json

csharp 复制代码
npx tsc --init

tsconfig.json

json 复制代码
{
  "compilerOptions": {
    "target": "ESNext",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    /* Modules */
    "module": "NodeNext",                                /* Specify what module code is generated. */
    "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    "outDir": "./dist",                                   /* Specify an output folder for all emitted files. */
  }
}

创建一个 cli的入口

bin/index.js

js 复制代码
#!/usr/bin/env node
import cac from 'cac'

import run from '../dist/index.js'

const cli = cac()

cli.command('', '默认命令')
  .action(() => {
    run()
  })

cli.help()

cli.version('0.0.1')

cli.parse()

在 package.json中指定关键字 bin

json 复制代码
"bin": {
    "re-import": "./bin/index.js"
},

在 package.json 中指定 scripts, 因为我们本地需要调试,所以开始 watch 监听模式

json 复制代码
"scripts": {
    "watch": "tsc --watch"
},

在 src/index.ts

随便写点内容, 启动 npm run watch

然后 npm link 注册为本地全局包,这块就不讲了,包的调试等相关内容

如果没有问题,就开始正文, 代码如下

src/index.ts

typescript 复制代码
import path from "node:path"
import { globSync } from "glob"
import inquirer from "inquirer"
import fs from "node:fs"

import transform from "./transform.js"

const projectRoot = process.cwd()

/**
 * 确认项目的目录
 */
const ensureProject = async () => {
  try {
    let answers = await inquirer.prompt([
      {
        type: "confirm",
        name: "root",
        message: "项目运行根目录: " + " " + projectRoot,
        default: true
      },
      {
        type: "input",
        name: "route",
        message: "路由路径",
        validate(vlaue: string | null) {
          if (!vlaue) return "请输入项目路由地址"

          // 验证路由文件是否存在
          const p = path.resolve(projectRoot, vlaue)

          try {
            fs.statSync(p)
            return true
          } catch (error) {
            return "路径不存在"
          }
        }
      }
    ])

    if (!answers.root) return false

    return answers.route
  } catch (error) {
    return false
  }
}

/**
 * 拿到所有的路由信息
 */
const getFiles = (p: string): Array<string> => {
  // 如果是文件,就直接读取这个文件,如果是目录,就拿到整个目录里面所有的js文件
  try {
    const stats = fs.statSync(p)

    if (stats.isDirectory()) {
      const jsFiles = globSync(`${projectRoot}/${p}/**/*.js`, {
        // 返回绝对地址
        absolute: true
      })

      return jsFiles
    } else {
      // 文件
      return [path.resolve(projectRoot, p)]
    }
  } catch (error) {
    return []
  }
}

export default async () => {
  let routesPath = await ensureProject()

  if (!routesPath) {
    console.log("信息验证失败 或放弃")
    console.log(routesPath)
    return
  }

  const files = getFiles(routesPath)

  console.log("拿到的所有路由文件:", `【${files.length}】个`)

  console.log(files)

  transform(files)
}
  1. 先询问用户,所使用的环境对不对,是不是项目根目录, 拿到路由文件地址信息
  2. 拿到所有的路由文件信息,可能是单个文件,可能是一个目录里面所有的文件,这里只处理js文件
  3. 读取内容,转换

我们现在看看代码转换的过程

src/transform.ts

typescript 复制代码
import generate from "@babel/generator"
import { parse } from "@babel/parser"
import traverse, { NodePath } from "@babel/traverse"
import * as types from "@babel/types"
import * as prettier from "prettier"

import fs from "node:fs"

let prettierOptions: prettier.Options | null = null

/**
 * 解析并读取项目根目录的prettier配置
 */
const resolvePrettierOptions = async () => {
  try {
    const prettierConfigFile = await prettier.resolveConfigFile()

    if (prettierConfigFile) {
      const options = await prettier.resolveConfig(prettierConfigFile)
      console.log("项目的配置文件对象")
      console.log(options)

      prettierOptions = options
    }
  } catch (error) {
    console.log("prettier 解析配置文件")
  }
}

// 改动过的文件
const modifyFiles = new Set()

/**
 * 重新生成一个箭头函数
 * @param str
 */
function genArrowFunction(str: string): types.ArrowFunctionExpression {
  return types.arrowFunctionExpression(
    [],
    types.importExpression(types.stringLiteral(str))
  )
}

const changeByArrowFunctionExpression = (
  path: NodePath<types.ArrowFunctionExpression>,
  file: String
) => {
  // 从函数调体里面拿到 组件的路径

  const node = path.node as types.ArrowFunctionExpression

  const body = node.body as types.CallExpression

  if (!body.arguments) return

  if (types.isArrayExpression(body.arguments[0])) {
    let elements = body.arguments[0].elements as Array<types.StringLiteral>

    // 拿到路径字符串
    let routePathStr = elements[0].value

    if (!routePathStr) return

    // 重新构造一个AST节点。 将这块内容替换为 import写法
    const newNode = genArrowFunction(routePathStr)

    // 节点替换
    path.replaceWith(newNode)

    modifyFiles.add(file)
  }
}

const genTasks = (item: string) => {
  return new Promise(async (resolve) => {
    const content = fs.readFileSync(item, {
      encoding: "utf-8"
    })

    let ast = parse(content, {
      sourceType: "module",
      sourceFilename: item
    })

    traverse.default(ast, {
      /**
       * 监听箭头函数
       */
      ArrowFunctionExpression(p: NodePath<types.ArrowFunctionExpression>) {
        changeByArrowFunctionExpression(p, item)
      }
    })

    if (modifyFiles.has(item)) {
      const code = generate.default(ast).code

      // 将代码格式化一下 - 如果解析到项目根目录有 prettier的配置
      if (prettierOptions) {
        let res = await prettier.format(code, {
          ...prettierOptions,
          // 让prettier自行根据文件路径的后缀决定使用什么  parser解析器解析代码(可兼容ts,js, jsx等场景)
          filepath: item
        })

        // 将修改后的内容写入到文件
        fs.writeFileSync(item, res)
      } else {
        // 将修改后的内容写入到文件
        fs.writeFileSync(item, generate.default(ast).code)
      }
    }

    resolve(void 0)
  })
}

export default async (list: Array<string>) => {
  await resolvePrettierOptions()

  const allTasks = list.map((item) => genTasks(item))

  try {
    await Promise.all(allTasks)

    console.log("转换完成")

    let arr = Array.from(modifyFiles)

    console.log("有require的文件清单", `【${arr.length}】个`)
    console.log(arr)
  } catch (error) {
    console.log("转换失败")

    console.log(error)
  }
}

这块内容就很简单, 已知文件路径, 就读取文件即可,然后把字符串转成 AST, 处理以后,使用 prettier 格式化,再把内容写入原来的文件

  1. 先解析一下 prettier 配置文件, 因为不同的项目,prettier的风格不一样,不能使用 babel 默认生成代码的风格 - 这一步主要是减少代码的 更改,减少代码在合并到主分支以后的冲突问题
  2. 包装一个 promise, 单个任务跟 上一章节讲的是一样的, 这里稍微改造了一下
    • 创建了一个 Set, 我们只转换更改过的文件, 其他文件,如果写法没有问题,不动(减少代码合并到主分支的冲突)
    • prettier 获取项目的配置文件,如果存在,则解析到配置选项,然后格式化, 拿到格式化后的内容,写入文件
  3. 批量执行转换

好了, 所有内容都编写完成,我们开始运行一下

运行

shell 复制代码
npm run watch

此时,ts已经被编译成js文件,且在 dist 目录下。

注册到全局

bash 复制代码
npm link

找一个项目试一下

arduino 复制代码
re-import

效果图:

到此,就结束了。

总结

从整个案例中,你可以学习到如下内容

  • 终端/命令行 cli 开发
  • babel AST 综合使用
  • ts 开发
  • npm 包开发
  • prettier API 使用
  • 基础的node fs 文件读写,文件判断, 获取目录下所有的文件地址

相关链接

希望对你学习 babel 和 AST相关的内容有帮助

喜欢就点个赞吧 😊😊😊

相关推荐
燃先生._.7 分钟前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235241 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240252 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人3 小时前
前端知识补充—CSS
前端·css
GISer_Jing3 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245523 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v3 小时前
webpack最基础的配置
前端·webpack·node.js