本文在上一篇内容上进行进一步改造,包装成一个随处可用的 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
路由路径,输入一个要处理的 路径,可以是一个文件
- 有的项目,所有的 路由都放在一个文件, 可以指定文件
- 有的项目分了模块,拆分了不同的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)
}
- 先询问用户,所使用的环境对不对,是不是项目根目录, 拿到路由文件地址信息
- 拿到所有的路由文件信息,可能是单个文件,可能是一个目录里面所有的文件,这里只处理js文件
- 读取内容,转换
我们现在看看代码转换的过程
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 格式化,再把内容写入原来的文件
- 先解析一下 prettier 配置文件, 因为不同的项目,prettier的风格不一样,不能使用 babel 默认生成代码的风格 - 这一步主要是减少代码的 更改,减少代码在合并到主分支以后的冲突问题
- 包装一个 promise, 单个任务跟 上一章节讲的是一样的, 这里稍微改造了一下
- 创建了一个
Set
, 我们只转换更改过的文件, 其他文件,如果写法没有问题,不动(减少代码合并到主分支的冲突) - prettier 获取项目的配置文件,如果存在,则解析到配置选项,然后格式化, 拿到格式化后的内容,写入文件
- 创建了一个
- 批量执行转换
好了, 所有内容都编写完成,我们开始运行一下
运行
shell
npm run watch
此时,ts已经被编译成js文件,且在 dist 目录下。
注册到全局
bash
npm link
找一个项目试一下
arduino
re-import
效果图:
到此,就结束了。
总结
从整个案例中,你可以学习到如下内容
- 终端/命令行 cli 开发
- babel AST 综合使用
- ts 开发
- npm 包开发
- prettier API 使用
- 基础的node fs 文件读写,文件判断, 获取目录下所有的文件地址
相关链接
- babel: 将单行声明改为多行-AST - 掘金 (juejin.cn)
- babel: 轻松将箭头函数转换为普通函数-源码 - 掘金 (juejin.cn)
- babel: 转换require为 import 写法 - 掘金 (juejin.cn)
希望对你学习 babel 和 AST相关的内容有帮助
喜欢就点个赞吧 😊😊😊