0%

gogocode AST 抽象语法树修改器使用例子 (三)

接着上一篇 gogocode AST 抽象语法树修改器使用例子 (二), 这次我们的场景是 vue2 升级到 vue3。

gogocode 官方已经提供了一个 Vue2 到 Vue3 升级插件 使用方法也比较简单,只需要

1
gogocode -s ./src -t gogocode-plugin-vue -o ./src-out

正如官方说的,这个插件可以帮你完成 80% 的转换任务,但是生下来的 20% 的任务还是得我们自己完成。

本文将记录在转换过程中遇到的各种各样的问题,以及如何完成该项目剩下的 20% 的转换

场景

这个项目是 2 年前基于 vue-cli 和 vue2 搭建的一个 Sass 系统,前段时间成功的从 vue-cli 迁移到了 vite , 但是仅仅这样的优化还不够,我们希望能使用最新的 vue3 和 antd3,在此基础上来开发后续的功能。

使用 ls -lR | grep '.vue' |wc -l 统计了下 src 下面的 vue 文件,共有 487 个,这要是手动转换是不可能的。

第一次使用 gogocode

当第一次听到有 gogocode 这个东西时,我迫不及待的使用它将项目转换了下,结果在转换的过程中各种报错(注意,不是转换后运行报错,而是转换就没成功)。

总结了下原因,是因为这个项目经过几个开发者开发,有技术好的,也有技术差的,有按规范写的,也有不按规范写的,导致 gogocodevue 插件不能按预期处理这些糟糕的代码,这是完全可以理解的。

思路

再经过直接转换失败后,萌生出了一个想法,为什么不提前将这些不规范的代码转换后再交与 gogocode 处理呢?

于是我们将 vue2 到 vue3 转换的过程拆封成了一下几步

1
2
3
1. 预转换(处理一些不规范和 gogocode 无法识别的代码)
2. gogocode 转换
3. 再次转换 (处理一些 gogocode 转换后,我们并不满意的代码)

我们先按照思路简单写下大框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import glob from "glob";
import execSh from "exec-sh";
import fs from "fs";
import write from "write";

const files = glob.sync(`src/**/*.vue`);

for (let i = 0; i < files.length; i++) {
const filePath = files[i];

// 预处理每个文件
preTransform(filePath, filePath.replace("/src/", "/src-pre/"));
}

// 执行 gogocode
await execSh.promise(
`npx gogocode -s ./src-pre -t gogocode-plugin-vue -o ./src`,
{
cwd: path.resolve(__dirname, "../"),
stdout: process.stdout,
}
);

// after handler
for (let i = 0; i < files.length; i++) {
const filePath = files;

// 再次处理每个文件
afterTransform(filePath);
}

function preTransform(filePath, newPath) {
let source = fs.readFileSync(filePath, "utf-8");
write.sync(newPath, source, { overwrite: true });
}

function afterTransform() {
let source = fs.readFileSync(filePath, "utf-8");
write.sync(filePath, source, { overwrite: true });
}

preTransform

预处理方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function preTransform(filePath, newPath) {
let source = fs.readFileSync(filePath, "utf-8");

// https://github.com/thx/gogocode/issues/145
// 添加 { sourceType: 'module' } 就行了,但是这里就不处理了
// 如果每个 $ 都添加 { sourceType: 'module' } 这就太麻烦了
source = source.replace(/import\.meta\.env/g, "import_meta_env");

// 替换 moment(record.data[col.id] | record.data[col.key])
source = source.replace(
`moment(record.data[col.id] | record.data[col.key])`,
`moment(record.data[col.id] || record.data[col.key])`
);

// 替换 watch: { filters: {} }
// 下面这个代码,把 watch 里面的 filters 也匹配到了
// https://github.com/thx/gogocode/blob/8a324770d307990276851c5c78dcfc4f7b4932dc/packages/gogocode-plugin-vue/src/filters.js#L40
source = $(source, {
parseOptions: {
language: "vue",
},
})
.find("<script></script>")
.replace(
`watch:{filters:$_$1 , $$$}`,
`watch:{filters_temporary_preTransform:$_$1 , $$$}`
)
.root()
.generate();

// 替换
// export default mixins().extend({}) => export default {}
source = $(source, {
parseOptions: {
language: "vue",
},
})
.find("<script></script>")
.replace(
`export default mixins($$$1).extend({$$$2})`,
`const MixinsPreFormat = {$$$1};export default {$$$2}`
)
.root()
.generate();

// 替换
// export default mixins().extend({}) => export default {}
source = $(source, {
parseOptions: {
language: "vue",
},
})
.find("<script></script>")
.replace(`export default mixins().extend({$$$})`, `export default {$$$}`)
.root()
.generate();

// formatCodes 是 prettier 的 api,用来格式化代码
write.sync(newPath, formatCodes(source, "vue"), { overwrite: true });
}

export function formatCodes(code, type = "typescript") {
return prettier.format(code, { semi: false, parser: type });
}

afterTransform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function afterTransform(filePath) {
let source = fs.readFileSync(filePath, "utf-8");
source = source.replace(/filters_temporary_preTransform/g, "filters");
source = replaceMixinsPreFormat($, source);
source = replaceVuex2Pinia($, source);

// 这个一定要放到最后
// https://github.com/thx/gogocode/issues/145
source = source.replace(/import_meta_env/g, "import.meta.env");
write.sync(filePath, formatCodes(source, "vue"), { overwrite: true });
}

/**
* 将 vuex 的调用方式替换为 pinia
* @param $
* @param source
*/
function replaceVuex2Pinia($, source) {
let $source = $(source, {
parseOptions: {
language: "vue",
},
});
$source = transformMutationsAndActionsInVueFile($, $source).root();
$source = transformGettersAndStatesInVueFile($, $source).root();
$source = autoImportStore($, $source).root();

return $source.generate();
}

/**
* 将 MixinsPreFormat 放置到 export default {} 中
* @param $
* @param source
*/
function replaceMixinsPreFormat($, source) {
if (source.trim().length === 0) return source;
let $source = $(source, {
parseOptions: {
language: "vue",
},
});

let params = ``;
$source
.find("<script></script>")
.find(`const MixinsPreFormat = {$$$}`)
.each((item) => {
params = item.match["$$$$"].map((param) => $(param).generate()).join(",");
});

$source
.root()
.find("<script></script>")
.replace(`const MixinsPreFormat = {}`, "");

if (params.length > 0) {
return $source
.find("<script></script>")
.replace(
`export default {$$$}`,
`export default {$$$,mixins:[${params}]}`
)
.root()
.generate();
}

return source;
}

总结

通过对 preTransform 和 afterTransform 方法的扩充,最终达到了 gogocode 转换过程不在报错,而且转换后的代码,也达到了我们预期的效果