最近在迁移项目,对于一些资源的引用路径,需要换成一个新的路径。还好之前都把资源引用的方式,改成以
@/..
别名引入的方式,替换起来还算简单。先把新旧路径列出来,然后用 VSCode 的全局替换,因为就只有三种,CV 三次就好了。这种方法,在当时我认为是比较好的了,其实也有打算整个什么脚本啥的,但不知道从何做起。直到当天晚些时候,看到了神光大佬最新发布的一篇文章:写一个同事见了会打你的 Prettier 插件。直接对我醍醐灌顶,这不就是我想要的工具嘛!于是就打算写一个 prettier 插件,统一格式化路径,想法有了,开干!
最终的效果如上,点击保存后,自动将
path
进行替换。
对于一段普通的 import
代码,转成 AST
后,是下面这样:
import
中的 path
对应的就是每个 ImportDeclaration
节点中的 source.value
。
只需要把每个节点中的 source.value
按照指定的替换规则给替换一遍,然后再用替换后的 AST
去生成代码,这样就大功告成了。是不是很简单?完整的 preprocess
如下:
function myPreprocessor(code, options) {
const ast = parser.parse(code, {
plugins: ["jsx"],
sourceType: "module"
})
const importNodes = []
traverse(ast, {
ImportDeclaration(path) {
importNodes.push(clone(path.node))
path.remove()
}
})
// 写入自定义替换规则,用一个二维数组记录
// 数组第一项为匹配规则,第二项为替换后的路径
const rules = [
["^@/packages/mixins/", "@/mixins/"],
["^@/libs/assist", "@/utils/assist"],
["^@/views/editor/editor-props", "@/utils/editor-props"]
]
rules.forEach(rule => {
rule[0] = new RegExp(`${rule[0]}`)
})
importNodes.forEach((node, i) => {
for (const [rule, t] of rules) {
const value = node.source.value
if (value.match(rule)) {
node.source.value = value.replace(rule, t)
break
}
}
})
const newAST = types.file({
type: "Program",
body: importNodes
})
const newCode = generate(newAST).code + "\n" + generate(ast, { retainLines: true }).code
return newCode
}
看看效果如何。
咦~ 不对劲啊!怎么把 17 行的注释,又重新生成了一次 😦。排查了一下,貌似是因为 newAST
中最后一个 ImportDeclaration
节点中有一个 trailingComments
节点(即 // this is test
),而 ast
中第一个 FunctionDeclaration
节点的 leadingComments
中同样也有这个注释节点,导致就生成了两次,那么把最后一个 ImportDeclaration
节点中的 trailingComments
清空是不是就行了?先试一下。
importNodes.forEach((node, i) => {
for (const [rule, t] of rules) {
const value = node.source.value
if (value.match(rule)) {
node.source.value = value.replace(rule, t)
break
}
}
// 清空最后一个 import 节点的 trailingComments
if (i === importNodes.length - 1) {
node.trailingComments = null
}
})
看样子,没啥问题。不妨再看一个例子。
这次又把 import
代码间的注释重新生成了一份。(⊙﹏⊙) 总不能去比较每个注释节点,看看是否有相同的,然后再去重吧 🤡。转念一想,可以去看看 prettier-plugin-sort-imports
的源码啊,看看人家怎么处理的。Get ~
源码是用 TS 写的,虽然没写过 TS ,但是大致思路还是能看懂的,吭吭哧哧也算看明白了个大概。总体下来,处理思路大致是这样的:
- 先从
importNodes
中获取所有注释节点allCommentsFromImports
。 - 然后从源代码片段中,移除
importNodes
与allCommentsFromImports
,得到一份干净的源代码codeWithoutImportsAndInterpreter
。 - 最后,用
importNodes
生成的AST
重新生成的代码 +codeWithoutImportsAndInterpreter
。
大致思路有了,又从 prettier-plugin-sort-imports
那里借鉴了两个方法:getAllCommentsFromNodes
(获取 importNodes
中的所有注释)、removeNodesFromOriginalCode
(从源代码片段中移除某些节点)。
完整的 preprocess
如下:
function myPreprocessor(code, options) {
const ast = parser.parse(code, {
plugins: ["jsx"],
sourceType: "module"
})
const importNodes = []
traverse(ast, {
ImportDeclaration(path) {
importNodes.push(clone(path.node))
}
})
const rules = [
["^@/packages/mixins/", "@/mixins/"],
["^@/libs/assist", "@/utils/assist"],
["^@/views/editor/editor-props", "@/utils/editor-props"]
]
rules.forEach(rule => {
rule[0] = new RegExp(`${rule[0]}`)
})
importNodes.forEach((node, i) => {
for (const [rule, t] of rules) {
const value = node.source.value
if (value.match(rule)) {
node.source.value = value.replace(rule, t)
break
}
}
if (i === importNodes.length - 1) {
node.trailingComments = null
}
})
const newAST = types.file({
type: "Program",
body: importNodes
})
const allCommentsFromImports = getAllCommentsFromNodes(importNodes)
const nodesToRemoveFromCode = [...importNodes, ...allCommentsFromImports]
const codeWithoutImportsAndInterpreter = removeNodesFromOriginalCode(code, nodesToRemoveFromCode)
const newCode = generate(newAST).code + codeWithoutImportsAndInterpreter
return newCode
}
再来看看效果如何。完美!
最后,为了让它看起来更像一个插件,我把路径替换规则挪到了 prettier
的配置中:
// .prettierrc.js
const myPlugin = require("./plugins/index")
module.exports = {
tabWidth: 2,
arrowParens: "avoid",
singleQuote: true,
semi: true,
trailingComma: "none",
pathReplaceRules: ["^@/packages/mixins/", "@/mixins/", "^@/libs/assist", "@/utils/assist", "^@/views/editor/editor-props", "@/utils/editor-props"],
plugins: [myPlugin]
}
同时需要在插件中,添加自定义的配置项,不然 prettier 会忽略这个配置。
// plugin/index.js
module.exports = {
parsers: {
babel: {
...babelParsers.babel,
preprocess: myPreprocessor
}
},
options: {
pathReplaceRules: {
type: "path",
array: true,
category: "Global",
default: [{ value: [[]] }],
description: "Provide an rule to replace imports."
}
}
}
在 prettier 官方文档里翻了好久,也没找到如何将配置配成一个二维数组的形式,貌似只能是一维字符串数组的形式。不过,好在可以把配置数组按每两个进行分块,格式化成下面这样:
const rules = [
["^@/packages/mixins/", "@/mixins/"],
["^@/libs/assist", "@/utils/assist"],
["^@/views/editor/editor-props", "@/utils/editor-props"]
]
一个简单的算法题:《数组分块》 😁
function arrayChunk(arr, size) {
return arr.reduce(
(res, curr) => {
if (res[res.length - 1].length < size) {
res[res.length - 1].push(curr)
} else {
res.push([curr])
}
return res
},
[[]]
)
}
虽然看起来很怪,但是整体还是不错的,使用后,开发效率咔咔往上升!😜