🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!
当我们谈及前端代码解析、转换等方面,很自然地会想到 AST、Babel、Recast 等这些工具,他们让前端er的工作更加便利。不过,尽管这些工具已经非常优秀,但它们的使用仍然存在一定的复杂性。
如果你想让代码修改体验像魔法一样简单直接梭哈,那么你可以试试一个叫做 magicast 的神奇库。他是antfu大佬的又一新作,最近摸🐳偶然发现到了,它可以让你轻松地操作 JavaScript 或 TypeScript 代码,就像处理 JSON 数据一样简单。此外,它还有一些魔法功能,比如智能格式化、导入/导出模块操作等等,让你在修改代码时不再感到无力。让我们一起来看看 magicast 是如何让代码修改变得神奇起来的吧!
前置了解
在进入文章主题前,我们先对 Recast 和 Babel 前置了解下。
recast
Recast 是一个用于重构和分析 JavaScript 代码的库,它提供了一个能够解析和生成 AST 的接口,可以使用 JavaScript 或 TypeScript 编写 AST 操作器,而无需使用 Babel 进行转换。Recast 的主要应用场景包括代码重构、代码分析、代码生成、代码格式化等方面,尤其适用于构建工具和代码检查工具等开发场景。
以下是一个基于 recast 写的分析源码函数的 demo,进而了解函数设计的质量等,并且还可以基础此 ast 构建简单的函数说明文档。
import * as recast from 'recast'
const { parse } = recast;
// 定义待解析的代码字符串,其中包含 JSDoc 注释
const code = `
/**
* @param {number[]} nums - 输入的整数数组
* @param {number} target - 目标整数
* @return {number[]} - 数组中的两个数,它们的和等于目标整数
*/
function twoSum (nums, target) {
// your code
};
`;
// 使用 recast 的 parse 方法将代码字符串解析成 AST
const ast = parse(code);
// 定义一个数组来存储生成的文档信息
let documentation = [];
// 使用 recast 提供的 visit 方法,遍历 AST,找到对应的注释并提取信息
recast.visit(ast, {
visitFunctionDeclaration(path) {
// 获取该函数节点中的注释
const comments = path.node.comments;
// 将注释中的描述信息和参数信息分离出来
const [description, ...paramsAndReturns] = comments
.map((comment) => comment.value.trim())
.join("\n")
.split(/@param|@returns/);
// 解析参数信息
const params = paramsAndReturns
.filter((param) => param.includes("{"))
.map((param) => {
const [,type, name, ...description] = param.trim().split(/[\s{}]+/);
return { type, name, description: description.join(" ").trim() };
});
// 解析返回值信息
const returns = paramsAndReturns.find((param) => param.includes("@returns"));
const returnType = returns ? returns.split("{")[1].split("}")[0].trim() : null;
const returnDescription = returns
? returns.split("{")[2].trim()
: "No return value.";
// 将函数的文档信息存入文档数组中
documentation.push({
name: path.node.id.name,
description: description,
params: params,
returnType: returnType,
returnDescription: returnDescription,
});
// 不需要进一步访问该函数节点的子节点,返回 false
return false;
},
});
// 输出生成的文档
console.log(documentation);
Babel
Babel 是一个广泛使用的 JavaScript 编译器,主要用于将 ES6+ 代码转换为向后兼容的 JavaScript 代码,以及支持最新 JavaScript 特性的语法转换和代码优化等。Babel 的主要应用场景是支持最新的 JavaScript 语法、转换不同 JavaScript 版本之间的代码,以及支持各种插件和预设,从而满足我们在不同场景下的需求。
import babel from '@babel/core';
import fs from 'node:fs';
const code = `
const iKun = {
sing () {},
dance () {},
rap () {}
}
`;
// Babel 的配置选项,这里只配置了转换语法,没有配置插件和预设
const options = {
presets: ['@babel/preset-env'],
};
// 转换代码
const { code: transformedCode } = babel.transform(code, options);
fs.writeFile('./iKun.js', transformedCode, err => {
if (err) return console.log('你干嘛~');
console.log('Music!');
})
关联与区别
Recast 和 Babel 之间有一定的关联,Recast 使用了 Babel 的解析器和代码生成器,因此 Recast 可以轻松地与 Babel 结合使用,实现更全面的 JavaScript 代码转换和重构。Recast 和 Babel 并不是相同互斥的,它们可以相互搭配使用,从而为我们提供更强大的 JavaScript 代码转换和重构的能力。
Recast 和 Babel 都涉及到对 JavaScript 代码进行转换和重构,但它们的目的和应用场景略有不同。Recast 更专注于重构和分析代码,提供了更灵活的 API 和功能,让我们可以更自由(放纵)地操作代码。而 Babel 则更专注于语法转换和向后兼容,通过支持各种插件和预设,实现在不同场景下的 JavaScript 版本兼容性和语法转换的需求。
Magicast
Magicast 是一个基于 Recast 和 Babel 构建的 AST 解析库,它提供了简化、优雅和熟悉的语法,可以对JavaScript 和 TypeScript 源代码进行程序化修改。 Magicast 的特点包括:
- 优雅地修改 JS/TS 文件,并像 JSON 一样进行优雅写回;
- 导出/导入操作使模块的导入和导出操作更加简单;
- 函数参数轻松操作传递给函数调用的参数,如
defineConfig()
; - 智能格式化保留了原始代码的格式样式(引号、制表符等);
- 可读性,摆脱 AST 操作的复杂性,让您的代码超级易读。
# using yarn
yarn add --dev magicast
# using npm
npm install -D magicast
# using pnpm
pnpm add -D magicast
Magicast 内部替我们对模块 ast 做了层封装,基于 Proxy 对不同类型的 astNode 节点代理操作,最后返回一个 ProxifiedModule,我们可以以操作 JSON 的形式操作 ProxifiedModule,内部代理会监听我们对 ProxifiedModule 的不同操作通过策略模式进行 ast 的处理和转换。 Magicast 内部提供了parseModule, writeFile, generateCode 等方法操作模块,同时也在 magicast/helpers 中提供了deepMergeObject,addNuxtModule,addVitePlugin 等多个高级工具来简化常见的任务(他们可能会在未来被转移到一个单独的包)。
loadFile
使用 loadFile 加载模块,是一个 async 函数,内部基于 fs.promises 异步读取模块内容后传入 parseModule 进行解析和响应式代理,最后返回 ProxifiedModule。
import { loadFile } from 'magicast'
import { resolvePath } from './utils.js'
const mod = await loadFile(resolvePath('./kunkun.js'))
mod.exports.default.test = '测试用的'
const { code } = mod.generate()
console.log(code)
以下是源码的具体实现:
parseModule
可以使用 parseModule 进行模块字符串的解析,内部会使用 recast.parse 对字符串进行解析,并将 code 和 解析后的 AstNode 节点传递 给 proxifyModule,内部调用 createProxy 进行响应式代理后生成并返回 ProxifiedModule。
import { parseModule } from 'magicast'
const mod = parseModule(`
export default {
name: 'iKun',
hobbies: ["speak", "sing"],
birthday: Date.now(),
speak: speak.bind(),
sing: sing.bind()
};
`)
mod.exports.default.experience = '2.5 years'
const {$ast, generate} = mod
const code = generate()
console.log($ast, code)
以下是源码的具体实现:
image.png
generateCode
generateCode 可以对 ast 或者 ProxifiedModule 转换为 code 和 sourcemap,内部通过基于 recast.print 将 ast 进行转换。
上文中的 mod.generate 与它是完全一致的,会在 parseModule 阶段将 generateCode 赋给响应式模块下的 generate
import { parseModule, generateCode } from 'magicast'
const mod = parseModule(`
export default {
name: 'iKun',
hobbies: ["speak", "sing"],
birthday: Date.now(),
speak: speak.bind(),
sing: sing.bind()
};
`)
mod.exports.default.experience = '2.5 years'
console.log('mod>>>>>>>\n', generateCode(mod).code)
console.log('ast>>>>>>>\n', generateCode(mod.$ast).code)
console.log('generate>>>>>>>\n', mod.generate().code)
以下是 generateCode 的具体实现
image.png
writeFile
writeFile 可以将 ProxifiedModule 或者 ast 转化和生成为 code 和 sourcemap 文件中,内部基于 generateCode 和 fs.promises.writeFile 实现。(偷个懒,例子就不贴了嘤嘤嘤)
以下是 writeFile 的具体实现
builders
builders 可以创建一个函数调用节点,创建一个新的表达式节点,值(RegExp,Set,Date...)的代理版本以及解析一个原始表达式并返回它的代理版本。 内部基于 recast.types.builders 实现,用于创建新的 AST 节点,允许你以可读性强、易于理解的方式构建 AST 节点,使代码分析和修改变得更加简单和高效。通过调用 builders 中提供的方法,你可以快速创建一个新的 AST 节点,并设置其属性和值,从而构建出一个完整的 AST 表示。
Example:
import { builders, generateCode } from "magicast";
const call = builders.newExpression("Foo", 1, "bar", {
foo: "bar",
});
const mod = parseModule("");
mod.exports.a = call;
const {code} = generateCode(mod)
console.log(code)
// "export const a = new Foo(1, \"bar\", {
// foo: \"bar\",
// });"
import { parseModule, builders, generateCode } from "magicast";
const call = builders.functionCall("functionName", 1, "bar", {
foo: "bar",
});
const mod = parseModule("");
mod.exports.a = call;
const {code} = generateCode(mod)
console.log(code)
// "export const a = functionName(1, \"bar\", {
// foo: \"bar\",
// });"
import { builders, parseModule, generateCode } from "magicast";
const expression = builders.raw("{ foo: 1 }");
const mod = parseModule("");
mod.exports.a = expression;
const {code} = generateCode(mod)
console.log(code)
// "export const a = {
// foo: 1,
// };"
heplers
目前内置提供的高级工具有 deepMergeObject,getDefaultExportOptions,addNuxtModule,addVitePlugin,未来可能会将这些工具单独提取出来。 感兴趣的小伙伴可以去仓库了解一下哟(点击直达)
小试牛刀
以下例子仅适用于熟悉 API 的使用和娱乐,上庭勿晒!!!
import { loadFile, builders } from "magicast";
import { getIKunConfig, loadSkill, createIKun, resolvePath } from './utils.js'
// 加载 Ikun 主体
const iKun = await loadFile(resolvePath('./IKun.ts'))
const { hobbies } = getIKunConfig()
const target = iKun.exports.default
target.hobbies = hobbies
target.birthday = builders.functionCall('Date.now')
// 加载技能包
loadSkill(
iKun,
hobbies,
'./skillShop.js'
)
// 给 iKun 注入灵魂
createIKun({
mod: iKun,
output: resolvePath('kunkun.js'),
printCode: true
})
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseModule, writeFile, generateCode, builders } from 'magicast'
export function getIKunConfig() {
// 利用 parseModule 将转换模块
const mod = parseModule(`
export const config = {
hobbies: ['sing', 'dance', 'rap', 'speak']
}
`);
// 哈哈哈,笑死
const hobbies = [...mod.exports.config.hobbies].sort(() => Math.random() - 0.5).slice(0, 2)
return {
hobbies
}
}
// 加载 Ikun 技能
export function loadSkill(target, skillNames, packageName) {
skillNames.forEach(skill => {
target.imports.$add({
// 导入变量
imported: skill,
// 依赖包名
from: packageName
});
target.exports.default[skill] = builders.functionCall(`${skill}.bind`)
})
}
// 创建 Ikun
export async function createIKun({mod, output, printCode = false}) {
try {
if (!mod || !output) await Promise.resolve('请检查 mod | output 参数格式')
// 将模块信息转化为代码并写入文件
await writeFile(mod, output)
console.log('[Music~]')
if (!!printCode) {
const {code} = generateCode(mod)
// 也可以这样使用
// const {code} = mod.generate()
console.log(`[show code]>>>>>>>>>>\n\n${code}\n\n>>>>>>>>>>`)
}
} catch (err) {
console.log('[你干嘛~]', err)
}
}
export function resolvePath(filePath) {
const dirname = path.dirname(fileURLToPath(import.meta.url))
return path.resolve(dirname, filePath)
}
export default {
name: 'IKun'
}
最终代码运行如下
文末感想
以上是对 magicast 这个库的初步了解,magicast 最让我觉得舒服的点就是可以像 JSON 数据操作回写模块代码,修改代码时不再感到无力,不过目前 magicast 处于初步的状态,存在较多问题和很多没有支持到位,但还是非常期待后续的更新!!🧀🧀 最后,希望这篇文章主要是想大家一起分享下有趣好玩的技术,如果文章中存在错误的内容希望请谅解俺这个小菜鸡,同时也会第一时间修正哈哈~
转载自:https://juejin.cn/post/7220743669804318775