手写rollup
初始化项目安装依赖
yarn init -y
yarn add acorn magic-string
添加src/main.js
var a = 1
var b = 2
添加debugger.js
const path = require('path')
const rollup = require('./lib/rollup')
let entry = path.resolve(__dirname,'src/main.js')
rollup(entry,'bundle.js')
添加lib/rollup.js
const Bundle = require('./bundle')
function rollup(entry,outputFileName){
const bundle = new Bundle({entry})
bundle.build(outputFileName)
}
module.exports = rollup
添加lib/bundle.js
const fs = require('fs')
const { default: MagicString } = require('magic-string')
const Module = require('./moudle')
class Bundle{
constructor(options){
this.entryPath = options.entry.replace(/\.js$/,'')+ '.js'
}
build(outputFileName){
let entryModule = this.fetchModule(this.entryPath)
this.statements = entryModule.expandAllStatements()
const { code } =this.generate()
fs.writeFileSync(outputFileName,code,'utf-8')
}
fetchModule(improtee){
let route = improtee
if(route){
let code = fs.readFileSync(route,'utf-8')
let module = new Module({
code,
path:route,
bundle:this
})
return module
}
}
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()}
}
}
module.exports = Bundle
添加lib/moudle.js
const { parse } = require("acorn")
const { default: MagicString } = require("magic-string")
const analyse = require("./ast/analyse")
class Module {
constructor({code,path,bundle}){
this.code = new MagicString(code,{filename:path})
this.path = path
this.bundle = bundle
this.ast = parse(code,{
ecmaVersion:7,
sourceType:'module'
})
this.analyse()
}
analyse(){
analyse(this.ast,this.code,this)
}
expandAllStatements(){
let allStatements = []
this.ast.body.forEach(statement => {
let statements = this.expandStatements(statement)
allStatements.push(...statements)
});
return allStatements
}
expandStatements(statement){
let result = []
if(!statement._included){
statement._included = true
result.push(statement)
}
return result
}
}
module.exports = Module
添加lib/ast/analyse.js
function analyse(ast,magicString,module){
ast.body.forEach(statement => {
Object.defineProperties(statement,{
_source:{
value:magicString.snip(statement.start,statement.end)
}
})
});
}
module.exports = analyse
出现下图说明打包成功
rollup的tree-sharking
修改main.js
import {name,age} from './msg'
function say(){
console.log('hello',name)
}
say()
export const name = 'xiaoshunshi'
export const age = 19
添加作用域函数scope.js
class Scope{
constructor(options = {}){
this.name = options.name
this.parent = options.parent
this.names = options.params || []
}
add(name){
this.names.push(name)
}
findDefiningScope(name){
if(this.names.includes(name)){
return this
}
if(this.parent){
return this.parent.findDefiningScope(name)
}
return null
}
}
module.exports = Scope
添加遍历节点方法walk.js
function walk(ast,{enter,leave}){
visit(ast,null,enter,leave)
}
function visit(node,parent,enter,leave){
if(enter){
enter.call(null,node,parent)
}
let childKeys = Object.keys(node).filter(key => typeof node[key] === 'object')
childKeys.forEach(childKey=>{
let value = node[childKey]
if(Array.isArray(value)){
value.forEach(val=>visit(val,node,enter,leave))
}else if(value && value.type){
visit(value,node,enter,leave)
}
})
if(leave){
leave.call(null,node,parent)
}
}
module.exports = walk
修改lib/ast/analyse.js
const Scope = require("./scope");
const walk = require("./walk");
function analyse(ast, magicString, module) {
let scope = new Scope()
ast.body.forEach(statement => {
function addToScope(declaration) {
var name = declaration.id.name
scope.add(name)
if (!scope.parent) {
statement._defines[name] = true
}
}
Object.defineProperties(statement, {
_defines: {
value: {}
},
_dependsOn: {
value: {}
},
_included: {
value: false,
writable: true
},
_source: {
value: magicString.snip(statement.start, statement.end)
}
})
walk(statement, {
enter(node) {
let newScope
switch (node.type) {
case 'FunctionDeclaration':
const params = node.params.map(x => x.name)
if(node.type === 'FunctionDeclaration'){
addToScope(node)
}
newScope = new Scope({
parent: scope,
params
})
break;
case 'VariableDeclaration':
node.declarations.forEach(addToScope)
break;
default:
break;
}
if(newScope){
Object.defineProperty(node,'_scope',{
value:newScope
})
scope = newScope
}
},
leave(node) {
if(node._scope){
scope = scope.parent
}
}
})
})
ast.body.forEach(statement=>{
walk(statement,{
enter(node){
if(node._scope){
scope = node._scope
}
if(node.type === 'Identifier'){
const definingScope = scope.findDefiningScope(node.name)
if(!definingScope){
statement._dependsOn[node.name] = true
}
}
},
leave(node){
if(node._scope){
scope = scope.parent
}
}
})
})
}
module.exports = analyse
修改lib/bundle.js
const fs = require('fs')
const { default: MagicString } = require('magic-string')
const Module = require('./moudle')
const path = require('path')
class Bundle{
constructor(options){
this.entryPath = options.entry.replace(/\.js$/,'')+ '.js'
}
build(outputFileName){
let entryModule = this.fetchModule(this.entryPath)
this.statements = entryModule.expandAllStatements()
const { code } =this.generate()
fs.writeFileSync(outputFileName,code,'utf-8')
}
fetchModule(improtee,importer){
let route = improtee
if(!importer){
route = improtee
}else{
if(path.isAbsolute(improtee)){
route = improtee
}else if(improtee[0] == '.'){
route = path.resolve(path.dirname(importer),improtee.replace(/\.js$/,'') + '.js')
}
}
if(route){
let code = fs.readFileSync(route,'utf-8')
let module = new Module({
code,
path:route,
bundle:this
})
return module
}
}
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()}
}
}
module.exports = Bundle
修改lib/moudle.js
const { parse } = require("acorn")
const { default: MagicString } = require("magic-string")
const analyse = require("./ast/analyse")
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop)
}
class Module {
constructor({code,path,bundle}){
this.code = new MagicString(code,{filename:path})
this.path = path
this.bundle = bundle
this.ast = parse(code,{
ecmaVersion:7,
sourceType:'module'
})
this.analyse()
}
analyse(){
this.imports = {}
this.exports = {}
this.ast.body.forEach(node =>{
if(node.type === 'ImportDeclaration'){
let source = node.source.value
let specifiers = node.specifiers
specifiers.forEach(specifier =>{
const name = specifier.imported.name
const loaclName = specifier.local.name
this.imports[loaclName] = {
name,
loaclName,
source
}
})
}else if(node.type === 'ExportNamedDeclaration'){
let declaration = node.declaration
if(declaration.type === 'VariableDeclaration'){
let name = declaration.declarations[0].id.name
this.exports[name] = {
node,localName:name,expression:declaration
}
}
}
})
analyse(this.ast,this.code,this)
this.definitions = {}
this.ast.body.forEach(statement=>{
Object.keys(statement._defines).forEach(name=>{
this.definitions[name] = statement
})
})
}
expandAllStatements(){
let allStatements = []
this.ast.body.forEach(statement => {
if(statement.type === 'ImportDeclaration'){
return
}
let statements = this.expandStatements(statement)
allStatements.push(...statements)
});
return allStatements
}
expandStatements(statement){
let result = []
const dependecies = Object.keys(statement._dependsOn)
dependecies.forEach(name=>{
let definition = this.define(name)
result.push(...definition)
})
if(!statement._included){
statement._included = true
result.push(statement)
}
return result
}
define(name){
if(hasOwnProperty(this.imports,name)){
const improtData = this.imports[name]
const module = this.bundle.fetchModule(improtData.source,this.path)
const exportData = module.exports[improtData.name]
return module.define(exportData.localName)
}else{
let statement = this.definitions[name]
if(statement && !statement._included){
return this.expandStatements(statement)
}else{
return []
}
}
}
}
module.exports = Module