手搓Rollup ▶️
前言
Rollup Github,开始照猫画虎 ,手写一个rollup,下面是一些前置知识,也是rollup用到的一些库
magic-string
magic-string是一个操作字符串和生成source-map的工具
const MagicString = require('magic-string');
const sourceCode = `export const name = 'magic-string'`;
const ms = new MagicString(sourceCode);
console.log('snip----', ms.snip(0, 6).toString());
console.log('remove----', ms.remove(0, 7).toString());
console.log('update----', ms.update(7, 12, 'let').toString());
const bundle = new MagicString.Bundle();
bundle.addSource({
content: `const a = 'magic-string'`,
separator: '\n'
});
bundle.addSource({
content: `const b = 'magic-string'`,
separator: '\n'
});
console.log('bundle----\n', bundle.toString());AST 抽象语法树
js代码编译后会被转成AST树(抽象语法树),这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作
AST工作流
- Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
- Transform(转换) 对抽象语法树进行转换
- Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
acorn
- 使用 acorn 库解析 js代码
import $ from 'jquery' - AST 遍历实现
- 实现了自定义的 AST 遍历函数 walk 和 visit
- 支持进入节点(
enter)和离开节点(leave)两种回调 - 递归遍历所有子节点
- 遍历过程
- 对 AST 中的每个节点执行深度优先遍历
- 打印每个节点的进入和离开事件及节点类型
const acorn = require('acorn');
const sourceCode = `import $ from 'jquery'`;
const ast = acorn.parse(sourceCode, {
locations: true,
ranges: true,
sourceType: 'module',
ecmaVersion: 8
});
const walk = (astNode, {enter, leave}) => {
visit(astNode, null, enter, leave);
};
const visit = (astNode, parent, enter, leave) => {
if (enter) {
enter(astNode, parent);
}
const keys = Object.keys(astNode).filter(key =>
typeof astNode[key] === 'object'
);
keys.forEach(key => {
const value = astNode[key];
if (Array.isArray(value)) {
value.forEach(childNode => {
if (childNode.type) {
visit(childNode, astNode, enter, leave);
}
});
} else if (value && value.type && typeof value === 'object') {
visit(value, astNode, enter, leave);
}
});
if (leave) {
leave(astNode, parent);
}
};
ast.body.forEach(node => {
walk(node, {
enter(node) {
console.log('enter:', node.type);
}, leave(node) {
console.log('leave:', node.type);
}
});
});作用域
实现作用域的层次结构和变量查找功能
constructor(options):初始化作用域name:作用域名称parent:父作用域引用,形成作用域链names:当前作用域内定义的变量名数组
add(name):向当前作用域添加变量名findDefiningScope(name):查找定义指定变量的作用域- 首先在当前作用域查找
- 如果未找到且存在父作用域,则递归向上查找
- 如果找到则返回对应作用域,否则返回null
class Scope {
constructor(options = {}) {
// 作用域的名称
this.name = options.name;
// 父作用域
this.parent = options.parent;
// 此作用域内定义的变量
this.names = options.names || [];
}
add(name) {
this.names.push(name);
}
findDefiningScope(name) {
if (this.names.includes(name)) {
return this;
} else if (this.parent) {
return this.parent.findDefiningScope(name);
} else {
return null;
}
}
}
const a = 1;
function one() {
const b = 1;
function two() {
const c = 2;
console.log(a, b, c);
}
}
let globalScope = new Scope({name: 'global', names: [], parent: null});
let oneScope = new Scope({name: 'one', names: ['b'], parent: globalScope});
let twoScope = new Scope({name: 'two', names: ['c'], parent: oneScope});
console.log(
twoScope.findDefiningScope('a')?.name,
twoScope.findDefiningScope('b')?.name,
twoScope.findDefiningScope('c')?.name
);开始手搓 👏
主入口(配置文件 rollup.config.js)
entry入口文件output输出文件rollup函数(手搓rollup实现)
const path = require('path');
const rollup = require('../lib/rollup');
const entry = path.resolve(__dirname, 'main.js');
const output = path.resolve(__dirname, '../dist/bundle.js');
rollup(entry, output);主实现(rollup.js)
- 上一步定义的rollup函数传入的两个参数
- 同时将读取->转换->生成代码,抽离出去,也就是放在一个bundle类中
const Bundle = require('./bundle');
function rollup(entry, output) {
const bundle = new Bundle({entry});
bundle.build(output);
}
/* global module */
module.exports = rollup;打包类(bundle.js)
bundle.js负责整个打包流程的管理。接收入口文件路径并生成最终的打包文件
constructor(options)初始化打包器:接收配置选项,解析入口文件的绝对路径build(output)执行打包构建- 调用
fetchModule获取入口模块 - 通过
expandAllStatements展开所有需要的语句 - 调用
generate生成最终代码 - 将结果写入输出文件
- 调用
fetchModule(importer)加载模块- 读取指定路径的文件内容
- 创建并返回
Module实例
generate()生成最终代码- 使用
MagicString.Bundle处理代码合并 - 将所有语句添加到
bundle中 - 返回生成的代码字符串
- 使用
const fs = require('fs');
const path = require('path');
const MagicString = require('magic-string');
const Module = require('./module');
class Bundle {
constructor(options) {
this.entryPath = path.resolve(options.entry);
}
build(output) {
const entryModule = this.fetchModule(this.entryPath);
// console.log('entryModule:', entryModule);
this.statements = entryModule.expandAllStatements();
// console.log('statements:', this.statements);
const {code} = this.generate();
// console.log('code:', code);
fs.writeFileSync(output, code);
console.log('modify-rollup bundle success');
}
fetchModule(importer) {
let route = importer;
if (route) {
let code = fs.readFileSync(route, 'utf8');
return new Module({
code, path: importer, bundle: this
});
}
}
generate() {
let magicString = new MagicString.Bundle();
this.statements.forEach(statement => {
const source = statement._source.clone();
magicString.addSource({
content: source,
separator: '\n'
});
});
return {code: magicString.toString()};
}
}
/* global module */
module.exports = Bundle;- 在通过
fetchModule加载模块后,会去analyse里面解析,这个解析放到后面看 - 再通过
expandAllStatements去展开这个AST树后取到里面所有的语句,如下图debugger调试可以看到
解析模块类(module.js)
用于负责解析模块代码、分析依赖关系
constructor({code, path, bundle})初始化模块- 使用
MagicString包装源代码 - 调用
acorn.parse生成AST - 调用
analyse函数分析模块
- 使用
expandAllStatements()展开所有语句- 遍历AST的body节点
- 调用
expandStatement处理每个语句 - 返回所有需要包含的语句数组
expandStatement(statement)展开单个语句- 标记语句为已包含(
_included = true) - 返回语句数组
- 标记语句为已包含(
const MagicString = require('magic-string');
const analyse = require('./analyse');
const {parse} = require('acorn');
class Module {
constructor({code, path, bundle}) {
this.code = new MagicString(code, {filename: path});
this.path = path;
this.bundle = bundle;
this.ast = parse(code, {
ecmaVersion: 8,
sourceType: 'module'
});
analyse(this.ast, this.code, this);
}
expandAllStatements() {
let allStatements = [];
this.ast.body.forEach(statement => {
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}
expandStatement(statement) {
statement._included = true;
let result = [];
result.push(statement);
return result;
}
}
/* global module */
module.exports = Module;分析模块函数(analyse.js)
AST节点属性增强
- 为抽象语法树(AST)中的每个语句节点添加自定义属性
- 便于后续的代码处理和打包操作
| 添加属性 | 类型 | 用途 | 说明 |
|---|---|---|---|
| _included | 布尔值,默认为 false | 标记该语句是否需要包含在最终打包结果中 | 可写:writable: true,允许后续修改 |
| _module | 对象引用 | 指向所属的 module 对象 | 便于访问模块相关信息 |
| _source | 代码片段 | 存储该语句对应的原始代码文本 | 通过 code.snip(statement.start, statement.end) 提取 |
/**
* 分析抽象语法树,给statement定义属性
* @param {*} ast 抽象语法树
* @param {*} code 代码字符串
* @param {*} module 模块对象
* */
function analyse(ast, code, module) {
// 给statement定义属性
ast.body.forEach(statement => {
Object.defineProperties(statement, {
_included: {value: false, writable: true},
_module: {value: module},
_source: {value: code.snip(statement.start, statement.end)}
});
});
}
/* global module */
module.exports = analyse;这个就是遍历AST树之后给每条语句添加这三个属性,其中_source属性是通过code.snip(statement.start, statement.end) 提取的语句原始代码文本
测试
到这里完成了第一步,之后运行主入口的测试文件进行测试,这里还没有对代码进行如何处理,只是单纯的先转AST,再从AST取出源代码写入到输出文件。
tree-shaking
Tree-shaking 是一种基于 ES 模块静态结构的死代码消除技术
测试实现原理
以下列代码为例:main.js是主入口,他引用了message.js 模块,message.js模块中定义了两个变量name和age,在打包之后,bundle.js中只保留了name和age 这两个变量,而要去掉这些import和export的语句
import {age, name} from './message';
const say = () => {
console.log('name:', name);
console.log('age:', age);
};
say();export let name = 'modify';
export let age = 18;let name = 'modify';
let age = 18;
const say = () => {
console.log('name:', name);
console.log('age:', age);
};
say();分析模块函数(analyse.js)
在module.js构造函数中添加三个属性
this.imports = {};
this.exports = {};
// 存放本模块的定义变量的语句
this.definitions = {};接下来就是重头戏了,修改analyse.js函数,在遍历AST树的过程中(下面贴上了AST语法树的json格式,帮助理解代码) ,记录下每个模块的imports和exports,以及每个语句定义的变量
- 添加
_dependOn、_defines属性,分别存放本模块的依赖变量(即import导入的)和定义的变量 - 判断
import语句,即type==='ImportDeclaration'- 此时去取值
source.value,即模块的路径./message - 同时去
specifiers里面找imported.name也就是导入来的变量,和local.name也就是本模块使用的变量名 - 记录到
imports[local.name],表示在主入口main.js中使用了该变量(作为键),值为source即对应来源模块,importedName即为来源模块导出的名称
- 此时去取值
- 判断
export语句,即type==='ExportNamedDeclaration'- 此时去取值
specifiers里面的exported.name,即导出的变量名 - 记录到
exports[exported.name],表示在message.js模块中导出了该变量(作为键),值为exported.name即变量名
- 此时去取值
- 处理变量的作用域,也就是说如果我导入了一个
name但是在一个方法内部我也使用了一个局部的name,那么在打包的时候我们需要知道要去替换哪个name- 定义一个全局作用域链,这个全局是针对于入口文件的
- 开始判断,如果是函数声明就加在全局作用域链中,函数内部定义的变量加到函数作用域链中
- 变量声明加到全局作用域链中
main.js转成的AST树
{
"type": "Program",
"start": 0,
"end": 127,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 36,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 8,
"end": 11,
"imported": {
"type": "Identifier",
"start": 8,
"end": 11,
"name": "age"
},
"local": {
"type": "Identifier",
"start": 8,
"end": 11,
"name": "age"
}
},
{
"type": "ImportSpecifier",
"start": 13,
"end": 17,
"imported": {
"type": "Identifier",
"start": 13,
"end": 17,
"name": "name"
},
"local": {
"type": "Identifier",
"start": 13,
"end": 17,
"name": "name"
}
}
],
"source": {
"type": "Literal",
"start": 24,
"end": 35,
"value": "./message",
"raw": "'./message'"
}
},
{
"type": "VariableDeclaration",
"start": 38,
"end": 118,
"declarations": [
{
"type": "VariableDeclarator",
"start": 44,
"end": 117,
"id": {
"type": "Identifier",
"start": 44,
"end": 47,
"name": "say"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 50,
"end": 117,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 56,
"end": 117,
"body": [
{
"type": "ExpressionStatement",
"start": 60,
"end": 87,
"expression": {
"type": "CallExpression",
"start": 60,
"end": 86,
"callee": {
"type": "MemberExpression",
"start": 60,
"end": 71,
"object": {
"type": "Identifier",
"start": 60,
"end": 67,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 68,
"end": 71,
"name": "log"
},
"computed": false,
"optional": false
},
"arguments": [
{
"type": "Literal",
"start": 72,
"end": 79,
"value": "name:",
"raw": "'name:'"
},
{
"type": "Identifier",
"start": 81,
"end": 85,
"name": "name"
}
],
"optional": false
}
},
{
"type": "ExpressionStatement",
"start": 90,
"end": 115,
"expression": {
"type": "CallExpression",
"start": 90,
"end": 114,
"callee": {
"type": "MemberExpression",
"start": 90,
"end": 101,
"object": {
"type": "Identifier",
"start": 90,
"end": 97,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 98,
"end": 101,
"name": "log"
},
"computed": false,
"optional": false
},
"arguments": [
{
"type": "Literal",
"start": 102,
"end": 108,
"value": "age:",
"raw": "'age:'"
},
{
"type": "Identifier",
"start": 110,
"end": 113,
"name": "age"
}
],
"optional": false
}
}
]
}
}
}
],
"kind": "const"
},
{
"type": "ExpressionStatement",
"start": 120,
"end": 126,
"expression": {
"type": "CallExpression",
"start": 120,
"end": 125,
"callee": {
"type": "Identifier",
"start": 120,
"end": 123,
"name": "say"
},
"arguments": [],
"optional": false
}
}
],
"sourceType": "module"
}analyse.js
const walk = require('./walk');
const Scope = require('./scope');
const {hasOwnProperty} = require('./utils');
/**
* 分析抽象语法树,给statement定义属性
* @param {*} ast 抽象语法树
* @param {*} code 代码字符串
* @param {*} module 模块对象
* */
function analyse(ast, code, module) {
// 开始找import和export
ast.body.forEach(statement => {
Object.defineProperties(statement, {
_included: {value: false, writable: true},
_module: {value: module},
_source: {value: code.snip(statement.start, statement.end)},
// 依赖的变量
_dependOn: {value: {}},
// 存放本语句定义了哪些变量
_defines: {value: {}}
});
// 判断类型是否为ImportDeclaration,即import语句
if (statement.type === 'ImportDeclaration') {
// 找到其来源,即./message
let source = statement.source.value;
// 找到其导入的变量
statement.specifiers.forEach(specifier => {
let importedName = specifier.imported.name;
let localName = specifier.local.name;
// 表示import导入的变量
module.imports[localName] = {
source, importedName
};
});
}
// 判断类型是否为ExportNamedDeclaration,即export语句
else if (statement.type === 'ExportNamedDeclaration') {
const {declaration} = statement;
if (declaration && declaration.type === 'VariableDeclaration') {
const {declarations} = declaration;
declarations.forEach(item => {
const {name} = item.id;
module.exports[name] = {name};
});
}
}
});
// 模块全局作用域
let globalScope = new Scope({name: 'global', names: [], parent: null});
// 创建作用域链
ast.body.forEach(statement => {
// 把模块下的变量等加到全局作用域下
function addToScope(name) {
globalScope.add(name);
if (!globalScope.parent) {
statement._defines[name] = true;
module.definitions[name] = statement;
}
}
function checkForRead(node) {
if (node.type === 'Identifier') {
// 表示当前语句依赖了node.name
statement._dependOn[node.name] = true;
}
}
function checkForWriter(node) {
function addNode(node) {
const {name} = node;
statement._modifies[name] = true;
if (!hasOwnProperty(module.modifications, name)) {
module.modifications[name] = [];
}
module.modifications[name].push(statement);
}
if (node.type === 'AssignmentExpression') {
addNode(node.left);
} else if (node.type === 'UpdateExpression') {
addNode(node.argument);
}
}
walk(statement, {
enter(node) {
checkForRead(node);
checkForWriter(node);
let newScope;
switch (node.type) {
case 'FunctionDeclaration':
// case 'ArrowFunctionExpression':
// 先把函数名添加到当前作用域
addToScope(node.id.name);
// 把参数添加到当前作用域
const names = node.params.map(param => param.name);
// 创建函数的作用域
newScope = new Scope({name: node.id.name, names, parent: globalScope});
break;
case 'VariableDeclaration':
// 变量声明
node.declarations.forEach(declaration => {
addToScope(declaration.id.name);
});
break;
default:
break;
}
if (newScope) {
Object.defineProperty(node, '_scope', {value: newScope});
globalScope = newScope;
}
}, leave(node) {
if (hasOwnProperty(node, '_scope')) {
globalScope = globalScope.parent;
}
}
});
});
}
/* global module */
module.exports = analyse;解析模块类(module.js)
这里只需要调整一下AST树再展开还原成源码,先做第一遍展开,对import语句进行过滤
function expandAllStatements() {
let allStatements = [];
this.ast.body.forEach(statement => {
if (statement.type === 'ImportDeclaration') {
return;
}
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}再添加一个方法,用来区分变量是导入还是内部的,直接通过变量名去imports里面找,因为在analyse 中,把所有导入的变量都加到imports里面了,并且在每次找到一个导入的变量时,去遍历加载他导入的模块,然后进行递归,直到找到
function define(name) {
// 区分变量是导入还是内部的
if (hasOwnProperty(this.imports, name)) {
// 找到是哪个模块导入的
const {source, importedName} = this.imports[name];
const importModule = this.bundle.fetchModule(source, this.path);
const exportName = importModule.exports[importedName].name;
return importModule.define(exportName);
} else {
// 如果非导入模块,是本地模块的话,获取此变量的变量定义语句
let statement = this.definitions[name];
if (statement && !statement._included) {
return this.expandStatement(statement);
} else {
return [];
}
}
}- 标记已处理:首先将当前语句标记为已包含(避免重复处理)
- 收集依赖定义
statement._dependOn存储当前语句依赖的变量列表(例如使用了其他地方定义的变量)- 对每个依赖变量,通过 this.define(name) 递归获取其定义语句(可能来自当前模块或导入模块),并添加到结果中
- 作用:确保执行当前语句前,所有依赖的变量已被定义
- 将当前语句本身添加到结果数组(依赖已收集完毕,现在可以执行该语句)
- 收集变量修改语句
statement._defines存储当前语句定义的变量(例如 let a = 1 中的 a)- 对每个定义的变量,查找其后续修改语句(this.modifications),并递归展开这些修改语句(确保修改语句的依赖也被处理)
- 作用:确保变量定义后,所有对该变量的修改都被包含在执行序列中
function expandStatement(statement) {
statement._included = true;
let result = [];
const _dependOn = Object.keys(statement._dependOn);
_dependOn.forEach(name => {
let definitions = this.define(name);
result.push(...definitions);
});
result.push(statement);
// 找此语句定义的变量以及其修改的变量语句
const defines = Object.keys(statement._defines);
defines.forEach(name => {
// 找其修改语句
const modifications = hasOwnProperty(this.modifications, name) && this.modifications[name];
if (modifications) {
modifications.forEach(modification => {
if (!modification._included) {
const expanded = this.expandStatement(modification);
result.push(...expanded);
}
});
}
});
return result;
}打包类(bundle.js)
在generate方法中,添加判断排除导出的语句
if (statement.type === 'ExportNamedDeclaration') {
source.remove(statement.start, statement.declaration.start);
}自此也就完成了对导入导出的处理,回到了这一步测试实现原理
块级作用域
案例说明
使用以下代码段,当有使用var的时候需要进行变量提升,同时当使用let的时候是块级作用域,应该直接在执行打包的时候直接抛出异常
if (true) {
var flag = true;
}
console.log('flag:', flag);作用域链(scope.js)
添加一个block属性,用于判断当前作用域是否为块级作用域,同时在add方法中添加变量的时候,判断当前作用域是否为块级作用域,如果是,则添加到父作用域中(表示如果在会计作用于当中用到的是var ,要对这个var进行变量提升)
/**
* 添加变量到作用域
* @param {string} name 变量名
* @param {boolean} isBlockDeclaration 是否为块级声明
* */
function add(name, isBlockDeclaration) {
// 不是块级声明,且为块级作用域,进行变量提升
if (!isBlockDeclaration && this.block) {
this.parent.add(name, isBlockDeclaration);
} else {
this.names.push(name);
}
}分析模块函数(analyse.js)
先对函数声明(全局作用域)、变量声明(判断他是不是var)、块级作用域(直接提升)进行判断,并创建对应的作用域,下面代码是省略版
switch (node.type) {
case 'FunctionDeclaration':
newScope = new Scope({name: node.id.name, names, parent: globalScope, block: false});
break;
case 'VariableDeclaration':
node.declarations.forEach(declaration => {
const flag = node.kind === 'const' || node.kind === 'let';
addToScope(declaration.id.name, flag);
});
break;
case 'BlockStatement':
newScope = new Scope({name: 'block', names: [], parent: globalScope, block: true});
break;
default:
break;
}同时将作用域进行加到全局作用域(上级作用域)的方法也进行调整
function addToScope(name, isBlockDeclaration) {
globalScope.add(name, isBlockDeclaration);
if (!globalScope.parent || (!isBlockDeclaration && globalScope.block)) {
statement._defines[name] = true;
module.definitions[name] = statement;
}
}解析模块类(module.js)
在define方法中,对第一个else进行处理,判断是否是全局变量,不是的话抛出异常(因为前面排除了变量定义,所以这里是当使用这个变量的时候,此时还没有这个变量即表示该变量未定义,抛出异常)
// 如果非导入模块,是本地模块的话,获取此变量的变量定义语句
let statement = this.definitions[name];
if (statement) {
if (!statement._included) {
return this.expandStatement(statement);
} else {
return [];
}
} else {
// 排除掉系统变量
if (SYSTEM_VAR.includes(name)) {
return [];
} else {
throw new Error(`${name} is not defined`);
}
}变量重命名
存在问题&打包
首先假设现在有三个person.js,然后在main.js中引入,然后打包,打包结果如下:【bundel.js 】,这个结果显然不太合理,因为person变量被重复定义了,那么应该如何解决这个问题呢?
再看一下用rollup进行打包,结果如下【rollup-bundle.js】,这个结果的变量被重命名了
const person = 'person';
export const person1 = person + '1';import {person1} from './person1';
import {person2} from './person2';
import {person3} from './person3';
console.log('person1:', person1);
console.log('person2:', person2);
console.log('person3:', person3);const person = 'person';
const person1 = person + '1';
console.log('person1:', person1);
const person = 'person';
const person2 = person + '2';
console.log('person2:', person2);
const person = 'person';
const person3 = person + '3';
console.log('person3:', person3);const person$2 = 'person';
const person1 = person$2 + '1';
const person$1 = 'person';
const person2 = person$1 + '2';
const person = 'person';
const person3 = person + '3';
console.log('person1:', person1);
console.log('person2:', person2);
console.log('person3:', person3);打包类(bundle.js)
在进行构建打包结果之前添加deConflict方法,用于处理变量名冲突的问题
- 定义两个变量来记录全部定义的变量,同时记录下有变量名冲突的变量
- 遍历定义过的变量,判断是否和已定义的变量冲突,如果冲突,则将变量名进行重命名
module.reName方法用于对变量进行重命名,也就是在module.js类中添加了一个属性(对象),键是旧名字、值是新名字
function deConflict() {
// 定义过的变量
const defines = {};
// 变量名冲突的变量
const conflicts = {};
this.statements.forEach(statement => {
Object.keys(statement._defines).forEach(name => {
if (hasOwnProperty(defines, name)) {
conflicts[name] = true;
} else {
defines[name] = [];
}
defines[name].push(statement._module);
});
});
Object.keys(conflicts).forEach(name => {
const modules = defines[name];
// 最后一个用这个名字可以不做处理
modules.pop();
modules.forEach((module, index) => {
const replaceName = `${name}${modules.length - index}`;
module.reName(name, replaceName);
});
});
}随后在generate方法遍历语句中对变量进行重命名,对所有的定义的变量&所有引用依赖的变量合并(这里所有的变量进行重命名)
let replaceNames = {};
// 定义的变量、依赖的变量都要进行重命名
Object.keys(statement._defines).concat(Object.keys(statement._dependOn)).forEach(name => {
const canonicalName = statement._module.getCanonicalName(name);
if (canonicalName !== name) {
replaceNames[name] = canonicalName;
}
});在构建源码之前添加replaceIdentifier方法,用于对变量进行重命名,直接用magicString进行替换
this.replaceIdentifier(statement, source, replaceNames);
magicString.addSource({
content: source, separator: '\n'
});
function replaceIdentifier(statement, source, replaceNames) {
walk(statement, {
enter(node) {
// 表示是一个标识符,且是需要重命名的
if (node.type === 'Identifier' && node.name && replaceNames[node.name]) {
source.overwrite(node.start, node.end, replaceNames[node.name]);
}
}
});
}