likes
comments
collection
share

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

作者站长头像
站长
· 阅读数 37

当我们谈及前端代码解析、转换等方面,很自然地会想到 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);

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

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!');
})

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

关联与区别

Recast 和 Babel 之间有一定的关联,Recast 使用了 Babel 的解析器和代码生成器,因此 Recast 可以轻松地与 Babel 结合使用,实现更全面的 JavaScript 代码转换和重构。Recast 和 Babel 并不是相同互斥的,它们可以相互搭配使用,从而为我们提供更强大的 JavaScript 代码转换和重构的能力。

Recast 和 Babel 都涉及到对 JavaScript 代码进行转换和重构,但它们的目的和应用场景略有不同。Recast 更专注于重构和分析代码,提供了更灵活的 API 和功能,让我们可以更自由(放纵)地操作代码。而 Babel 则更专注于语法转换和向后兼容,通过支持各种插件和预设,实现在不同场景下的 JavaScript 版本兼容性和语法转换的需求。

Magicast

Magicast 是一个基于 RecastBabel 构建的 AST 解析库,它提供了简化、优雅和熟悉的语法,可以对JavaScript 和 TypeScript 源代码进行程序化修改。 Magicast 的特点包括:

  • 优雅地修改 JS/TS 文件,并像 JSON 一样进行优雅写回;
  • 导出/导入操作使模块的导入和导出操作更加简单;
  • 函数参数轻松操作传递给函数调用的参数,如 defineConfig()
  • 智能格式化保留了原始代码的格式样式(引号、制表符等);
  • 可读性,摆脱 AST 操作的复杂性,让您的代码超级易读。

仓库地址:github.com/unjs/magica…

# 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。 🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

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)

以下是源码的具体实现:

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

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)

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

以下是源码的具体实现:

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

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)

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

以下是 generateCode 的具体实现

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

image.png

writeFile

writeFile 可以将 ProxifiedModule 或者 ast 转化和生成为 code 和 sourcemap 文件中,内部基于 generateCode 和 fs.promises.writeFile 实现。(偷个懒,例子就不贴了嘤嘤嘤)

以下是 writeFile 的具体实现

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效!

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,未来可能会将这些工具单独提取出来。 感兴趣的小伙伴可以去仓库了解一下哟(点击直达

小试牛刀

🧙🏼【Magicast】让你的JS/TS代码变得魔法般高效! 以下例子仅适用于熟悉 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,
  outputresolvePath('kunkun.js'),
  printCodetrue
})
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(02)
  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】让你的JS/TS代码变得魔法般高效!

文末感想

以上是对 magicast 这个库的初步了解,magicast 最让我觉得舒服的点就是可以像 JSON 数据操作回写模块代码,修改代码时不再感到无力,不过目前 magicast 处于初步的状态,存在较多问题和很多没有支持到位,但还是非常期待后续的更新!!🧀🧀 最后,希望这篇文章主要是想大家一起分享下有趣好玩的技术,如果文章中存在错误的内容希望请谅解俺这个小菜鸡,同时也会第一时间修正哈哈~

转载自:https://juejin.cn/post/7220743669804318775
评论
请登录