跳到主要内容

模拟实现

2023年06月11日
柏拉文
越努力,越幸运

一、实现


1.1 /src/bundle.ts

import { Graph } from './graph';
import { Module } from './module';
import * as MagicString from 'magic-string';

interface BundleOptions {
entry: string;
}

export class Bundle {
graph: Graph;
constructor(options: BundleOptions) {
this.graph = new Graph({
entry: options.entry,
bundle: this
});
}

async build() {
await this.graph.build();
}

getModuleById(id: string) {
return this.graph.getModuleById(id);
}

addModule(module: Module) {
return this.graph.addModule(module);
}

render(): { code: string; map: MagicString.SourceMap } {
let msBundle = new MagicString.Bundle({ separator: '\n' });

this.graph.orderedModules.forEach((module) => {
msBundle.addSource({
content: module.render()
});
});

const map = msBundle.generateMap({ includeContent: true });
return {
code: msBundle.toString(),
map
};
}
}

1.2 /src/graph.ts

import { Module } from './module';
import { Bundle } from './bundle';
import { keys } from './utils/object';
import { dirname, resolve } from 'path';
import { ModuleLoader } from './moduleLoader';

interface GraphOptions {
entry: string;
bundle: Bundle;
}

export class Graph {
bundle: Bundle;
basedir: string;
entryPath: string;
modules: Module[] = [];
moduleLoader: ModuleLoader;
orderedModules: Module[] = [];
moduleById: Record<string, Module> = {};

constructor(options: GraphOptions) {
const { entry, bundle } = options;
this.entryPath = resolve(entry);
this.basedir = dirname(this.entryPath);
this.bundle = bundle;
this.moduleLoader = new ModuleLoader(bundle);
}

async build() {
// 1. 获取并解析模块信息
const entryModule = await this.moduleLoader.fetchModule(
this.entryPath,
null,
true
);
// 2. 构建依赖关系图
this.modules.forEach(module => module.bind());
// 3. 模块拓扑排序
this.orderedModules = this.sortModules(entryModule!);
// 4. 标记需要包含的语句
entryModule!.getExports().forEach(name => {
const declaration = entryModule!.traceExport(name);
declaration!.use();
});
// 5. 处理命名冲突
this.doconflict();
}

doconflict() {
const used: Record<string, true> = {};

function getSafeName(name: string) {
let safeName = name;
let count = 1;
while (used[safeName]) {
safeName = `${name}$${count++}`;
}
used[safeName] = true;
return safeName;
}

this.modules.forEach(module => {
keys(module.declarations).forEach(name => {
const declaration = module.declarations[name];
declaration.name = getSafeName(declaration.name!);
});
});
}

getModuleById(id: string) {
return this.moduleById[id];
}

addModule(module: Module) {
if (!this.moduleById[module.id]) {
this.moduleById[module.id] = module;
this.modules.push(module);
}
}

sortModules(entryModule: Module) {
const orderedModules: Module[] = [];
const analysedModule: Record<string, boolean> = {};
const parent: Record<string, string> = {};
const cyclePathList: string[][] = [];

function getCyclePath(id: string, parentId: string): string[] {
const paths = [id];
let currentId = parentId;
while (currentId !== id) {
paths.push(currentId);
// 向前回溯
currentId = parent[currentId];
}
paths.push(paths[0]);
return paths.reverse();
}

function analyseModule(module: Module) {
if (analysedModule[module.id]) {
return;
}
for (const dependency of module.dependencyModules) {
// 检测到循环依赖
if (parent[dependency.id]) {
if (!analysedModule[dependency.id]) {
cyclePathList.push(getCyclePath(dependency.id, module.id));
}
continue;
}
parent[dependency.id] = module.id;
analyseModule(dependency);
}
analysedModule[module.id] = true;
orderedModules.push(module);
}

analyseModule(entryModule);
if (cyclePathList.length) {
cyclePathList.forEach(paths => {
console.log(paths);
});
process.exit(1);
}
return orderedModules;
}
}

1.3 /src/module.ts

import { keys } from './utils/object';
import type { Bundle } from './bundle';
import MagicString from 'magic-string';
import { Statement } from './statement';
import { ModuleLoader } from './moduleLoader';

import {
Declaration,
SyntheticDefaultDeclaration,
SyntheticNamespaceDeclaration
} from './ast/declaration';
import {
parse,
Statement as StatementNode
} from '../../../ast/rollupAST/src/index';

export interface ModuleOptions {
path: string;
bundle: Bundle;
loader: ModuleLoader;
code: string;
isEntry: boolean;
}

interface ImportOrExportInfo {
source?: string;
localName: string;
name: string;
statement?: Statement;
isDeclaration?: boolean;
module?: Module;
}

interface Specifier {
type: string;
local: {
name: string;
};
imported: {
name: string;
};
exported: {
name: string;
};
}

type Imports = Record<string, ImportOrExportInfo>;
type Exports = Record<string, ImportOrExportInfo>;

export class Module {
isEntry: boolean = false;
id: string;
path: string;
bundle: Bundle;
moduleLoader: ModuleLoader;
code: string;
magicString: MagicString;
statements: Statement[];
imports: Imports;
exports: Exports;
reexports: Exports;
exportAllSources: string[] = [];
exportAllModules: Module[] = [];
declarations: Record<string, Declaration>;
dependencies: string[] = [];
dependencyModules: Module[] = [];
referencedModules: Module[] = [];
constructor({ path, bundle, code, loader, isEntry = false }: ModuleOptions) {
this.id = path;
this.bundle = bundle;
this.moduleLoader = loader;
this.isEntry = isEntry;
this.path = path;
this.code = code;
this.magicString = new MagicString(code);
this.imports = {};
this.exports = {};
this.reexports = {};
this.declarations = {};
try {
const ast = parse(code) as any;

const nodes = ast.body as StatementNode[];
this.statements = nodes.map(node => {
const magicString = this.magicString.snip(node.start, node.end);
return new Statement(node, magicString, this);
});
} catch (e) {
console.log(e);
throw e;
}
this.analyseAST();
}

analyseAST() {
this.statements.forEach(statement => {
statement.analyse();
if (statement.isImportDeclaration) {
this.addImports(statement);
} else if (statement.isExportDeclaration) {
this.addExports(statement);
}
// 注册顶层声明
if (!statement.scope.parent) {
statement.scope.eachDeclaration((name, declaration) => {
this.declarations[name] = declaration;
});
}
});
const statements = this.statements;
let next = this.code.length;
for (let i = statements.length - 1; i >= 0; i--) {
statements[i].next = next;
next = statements[i].start;
}
// console.log('module:', this.path, this.declarations);
}

addDependencies(source: string) {
if (!this.dependencies.includes(source)) {
this.dependencies.push(source);
}
}

addImports(statement: Statement) {
const node = statement.node as any;
const source = node.source.value;
// import
node.specifiers.forEach((specifier: Specifier) => {
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
const localName = specifier.local.name;
const name = isDefault
? 'default'
: isNamespace
? '*'
: specifier.imported.name;
this.imports[localName] = { source, name, localName };
});
this.addDependencies(source);
}

addExports(statement: Statement) {
const node = statement.node as any;
const source = node.source && node.source.value;
if (node.type === 'ExportNamedDeclaration') {
// export { a, b } from 'mod'
if (node.specifiers.length) {
node.specifiers.forEach((specifier: Specifier) => {
const localName = specifier.local.name;
const exportedName = specifier.exported.name;
this.exports[exportedName] = {
localName,
name: exportedName
};
if (source) {
this.reexports[localName] = {
statement,
source,
localName,
name: localName,
module: undefined
};
this.imports[localName] = {
source,
localName,
name: localName
};
this.addDependencies(source);
}
});
} else {
const declaration = node.declaration;
let name;
if (declaration.type === 'VariableDeclaration') {
// export const foo = 2;
name = declaration.declarations[0].id.name;
} else {
// export function foo() {}
name = declaration.id.name;
}
this.exports[name] = {
statement,
localName: name,
name
};
}
} else if (node.type === 'ExportDefaultDeclaration') {
const identifier =
// export default foo;
(node.declaration.id && node.declaration.id.name) ||
// export defualt function foo(){}
node.declaration.name;

this.exports['default'] = {
statement,
localName: identifier,
name: 'default'
};
this.declarations['default'] = new SyntheticDefaultDeclaration(
node,
identifier,
statement
);
} else if (node.type === 'ExportAllDeclaration') {
// export * from 'mod'
if (source) {
this.exportAllSources.push(source);
this.addDependencies(source);
}
}
}

bind() {
this.bindImportSpecifiers();
this.bindReferences();
}

bindImportSpecifiers() {
[...Object.values(this.imports), ...Object.values(this.reexports)].forEach(
specifier => {
specifier.module = this._getModuleBySource(specifier.source!);
}
);
this.exportAllModules = this.exportAllSources.map(
this._getModuleBySource.bind(this)
);
// 建立模块依赖图
this.dependencyModules = this.dependencies.map(
this._getModuleBySource.bind(this)
);
this.dependencyModules.forEach(module => {
module.referencedModules.push(this);
});
}

bindReferences() {
// 处理 default 导出
if (this.declarations['default'] && this.exports['default'].localName) {
const declaration = this.trace(this.exports['default'].localName);
if (declaration) {
(this.declarations['default'] as SyntheticDefaultDeclaration).bind(
declaration
);
}
}
this.statements.forEach(statement => {
statement.references.forEach(reference => {
// 根据引用寻找声明的位置
// 寻找顺序: 1. statement 2. 当前模块 3. 依赖模块
const declaration =
reference.scope.findDeclaration(reference.name) ||
this.trace(reference.name);
if (declaration) {
declaration.addReference(reference);
}
});
});
}
getOrCreateNamespace() {
if (!this.declarations['*']) {
this.declarations['*'] = new SyntheticNamespaceDeclaration(this);
}
return this.declarations['*'];
}
trace(name: string) {
if (this.declarations[name]) {
// 从当前模块找
return this.declarations[name];
}
if (this.imports[name]) {
const importSpecifier = this.imports[name];
const importModule = importSpecifier.module!;
if (importSpecifier.name === '*') {
return importModule.getOrCreateNamespace();
}
// 从依赖模块找
const declaration = importModule.traceExport(importSpecifier.name);
if (declaration) {
return declaration;
}
}
return null;
}

traceExport(name: string): Declaration | null {
// 1. reexport
// export { foo as bar } from './mod'
const reexportDeclaration = this.reexports[name];
if (reexportDeclaration) {
// 说明是从其它模块 reexport 出来的
// 经过 bindImportSpecifier 方法处理,现已绑定 module
const declaration = reexportDeclaration.module!.traceExport(
reexportDeclaration.localName
);
if (!declaration) {
throw new Error(
`${reexportDeclaration.localName} is not exported by module ${
reexportDeclaration.module!.path
}(imported by ${this.path})`
);
}
return declaration;
}
// 2. export
// export { foo }
const exportDeclaration = this.exports[name];
if (exportDeclaration) {
const declaration = this.trace(name);
if (declaration) {
return declaration;
}
}
// 3. export all
for (let exportAllModule of this.exportAllModules) {
const declaration = exportAllModule.trace(name);
if (declaration) {
return declaration;
}
}
return null;
}

render() {
const source = this.magicString.clone().trim();
this.statements.forEach(statement => {
// 1. Tree Shaking
if (!statement.isIncluded) {
source.remove(statement.start, statement.next);
return;
}
// 2. 重写引用位置的变量名 -> 对应的声明位置的变量名
statement.references.forEach(reference => {
const { start, end } = reference;
const declaration = reference.declaration;
if (declaration) {
const name = declaration.render();
source.overwrite(start, end, name!);
}
});
// 3. 擦除/重写 export 相关的代码
if (statement.isExportDeclaration && !this.isEntry) {
// export { foo, bar }
if (
statement.node.type === 'ExportNamedDeclaration' &&
statement.node.specifiers.length
) {
source.remove(statement.start, statement.next);
}
// remove `export` from `export const foo = 42`
else if (
statement.node.type === 'ExportNamedDeclaration' &&
(statement.node.declaration!.type === 'VariableDeclaration' ||
statement.node.declaration!.type === 'FunctionDeclaration')
) {
source.remove(
statement.node.start,
statement.node.declaration!.start
);
}
// remove `export * from './mod'`
else if (statement.node.type === 'ExportAllDeclaration') {
source.remove(statement.start, statement.next);
}
// export default
else if (statement.node.type === 'ExportDefaultDeclaration') {
const defaultDeclaration = this.declarations['default'];
const defaultName = defaultDeclaration.render();

// export default function() {} -> function a() {}
if (statement.node.declaration.type === 'FunctionDeclaration') {
if (statement.node.declaration.id) {
// export default function foo() {} -> const a = funciton foo() {}
source.overwrite(
statement.node.start,
statement.node.declaration.start,
`const ${defaultName} = `
);
} else {
source.overwrite(
statement.node.start,
statement.node.declaration.start + 8,
`function ${defaultName}`
);
}
} else {
// export default () => {}
// export default Foo;
source.overwrite(
statement.node.start,
statement.node.declaration.start,
`const ${defaultName} = `
);
}
}
}
});
// 4. 单独处理 namespace 导出
if (this.declarations['*']) {
const namespaceDeclaration = this.declarations[
'*'
] as SyntheticNamespaceDeclaration;
if (namespaceDeclaration.needsNamespaceBlock) {
source.append(`\n\n${namespaceDeclaration.renderBlock()}\n`);
}
}
return source.trim();
}

getExports(): string[] {
return [
...keys(this.exports),
...keys(this.reexports),
...this.exportAllModules
.map(module =>
module.getExports().filter((name: string) => name !== 'default')
)
.flat()
];
}

private _getModuleBySource(source: string) {
const id = this.moduleLoader.resolveId(source!, this.path) as string;
return this.bundle.getModuleById(id);
}
}

1.4 /src/moduleLoader.ts

import { Bundle } from './bundle';
import { Module } from './module';
import { readFile } from 'fs/promises';
import { defaultResolver } from 'utils/resolve';

export class ModuleLoader {
bundle: Bundle;
resolveIdsMap: Map<string, string | false> = new Map();
constructor(bundle: Bundle) {
this.bundle = bundle;
}

resolveId(id: string, importer: string | null) {
const cacheKey = id + importer;
if (this.resolveIdsMap.has(cacheKey)) {
return this.resolveIdsMap.get(cacheKey)!;
}
const resolved = defaultResolver(id, importer);
this.resolveIdsMap.set(cacheKey, resolved);
return resolved;
}
// 加载模块并解析
async fetchModule(
id: string,
importer: null | string,
isEntry = false,
bundle: Bundle = this.bundle,
loader: ModuleLoader = this
): Promise<Module | null> {
const path = this.resolveId(id, importer);

if (path === false) {
return null;
}
const existModule = this.bundle.getModuleById(path);
if (existModule) {
return existModule;
}
const code = await readFile(path, { encoding: 'utf-8' });
// 初始化模块,解析 AST
const module = new Module({
path,
code,
bundle,
loader,
isEntry
});
this.bundle.addModule(module);
// 拉取所有的依赖模块
await this.fetchAllDependencies(module);
return module;
}

async fetchAllDependencies(module: Module) {
await Promise.all(
module.dependencies.map((dep) => {
return this.fetchModule(dep, module.path);
})
);
}
}

1.6 /src/rollup.ts

import fs from 'node:fs'
import { Bundle } from './bundle';
import { dirname } from 'node:path'

type Error = NodeJS.ErrnoException | null

export interface RollupOptions {
input: string;
output: string;
}

const existsSync = (dirname: string) => {
return fs.existsSync(dirname)
}

const createDir = (path: string) => {
return new Promise((resolve, reject) => {
const lastPath = path.substring(0, path.lastIndexOf("/"));
fs.mkdir(lastPath, { recursive: true }, (error) => {
if (error) {
reject({ success: false })
} else {
resolve({ success: true })
}
});
})
}

const writeFile = (path: string, content: string, format: BufferEncoding = 'utf-8') => {
return new Promise((resolve, reject) => {
fs.writeFile(
path,
content,
{
mode: 438, // 可读可写666,转化为十进制就是438
flag: 'w+', // r+并不会清空再写入,w+会清空再写入
encoding: format,
},
(err: Error) => {
if (err) {
reject({ success: false, data: err })
} else {
resolve({ success: true, data: { path, content } })
}
},
)
})
}

export function rollup(options: RollupOptions) {
const { input = './index.js', output = './dist/index.js' } = options
const bundle = new Bundle({
entry: input
});
return bundle.build().then(() => {
const generate = () => bundle.render()
return {
generate,
write: async () => {
const { code, map } = generate();
if (!existsSync(dirname(output))) {
await createDir(output)
}
return Promise.all([
writeFile(output, code),
writeFile(output + '.map', map.toString())
]);
}
};
});
}

1.7 /src/statement.ts

import { Scope } from './ast/scope';
import MagicString from 'magic-string';
import type { Module } from './module';
import { Reference } from './ast/reference';
import { buildScope } from 'utils/buildScope';
import { findReference } from 'utils/findReference';
import {
Statement as StatementNode,
ExportDeclaration,
FunctionDeclaration,
ExportAllDeclaration,
ExportNamedDeclaration
} from '../../../ast/rollupAST/src/index';
import {
isFunctionDeclaration,
isExportDeclaration,
isImportDeclaration
} from 'utils/isFunctionDeclaration';

export class Statement {
// acorn type problem
node: StatementNode;
magicString: MagicString;
module: Module;
scope: Scope;
start: number;
next: number;
isImportDeclaration: boolean;
isExportDeclaration: boolean;
isReexportDeclaration: boolean;
isFunctionDeclaration: boolean;
isIncluded: boolean = false;
defines: Set<string> = new Set();
modifies: Set<string> = new Set();
dependsOn: Set<string> = new Set();
references: Reference[] = [];
constructor(node: StatementNode, magicString: MagicString, module: Module) {
this.magicString = magicString;
this.node = node;
this.module = module;
this.scope = new Scope({
statement: this
});
this.start = node.start;
this.next = 0;
this.isImportDeclaration = isImportDeclaration(node);
this.isExportDeclaration = isExportDeclaration(node as ExportDeclaration);
this.isReexportDeclaration =
this.isExportDeclaration &&
!!(node as ExportAllDeclaration | ExportNamedDeclaration).source;
this.isFunctionDeclaration = isFunctionDeclaration(
node as FunctionDeclaration
);
}

analyse() {
if (this.isImportDeclaration) return;
// 1、构建作用域链,记录 Declaration 节点表
buildScope(this);
// 2. 寻找引用依赖,记录 Reference 节点表
findReference(this);
}

mark() {
if (this.isIncluded) {
return;
}
this.isIncluded = true;
this.references.forEach(
(ref: Reference) => ref.declaration && ref.declaration.use()
);
}
}

1.8 /src/ast/declaration.ts

import { Module } from '../module';
import { Reference } from './reference';
import { Statement } from '../statement';
import { keys, values } from '../utils/object';

export class Declaration {
isFunctionDeclaration: boolean = false;
functionNode: any;
statement: Statement | null;
name: string | null = null;
isParam: boolean = false;
isUsed: boolean = false;
isReassigned: boolean = false;
constructor(node: any, isParam: boolean, statement: Statement | null) {
if (node) {
if (node.type === 'FunctionDeclaration') {
this.isFunctionDeclaration = true;
this.functionNode = node;
} else if (
node.type === 'VariableDeclarator' &&
node.init &&
/FunctionExpression/.test(node.init.type)
) {
this.isFunctionDeclaration = true;
this.functionNode = node.init;
}
}
this.statement = statement;
this.isParam = isParam;
}

addReference(reference: Reference) {
reference.declaration = this;
this.name = reference.name;
}

use() {
this.isUsed = true;
if (this.statement) {
this.statement.mark();
}
}

render() {
return this.name;
}
}

export class SyntheticDefaultDeclaration extends Declaration {
original: Declaration | null;
exportName: string | null;
constructor(node: any, name: string, statement: Statement) {
super(node, false, statement);
this.original = null;
this.exportName = '';
this.name = name;
}

render() {
return this.original?.render() || this.name;
}

bind(declaration: Declaration) {
this.original = declaration;
}
}

export class SyntheticNamespaceDeclaration extends Declaration {
module: Module;
originals: Record<string, Declaration> = {};
needsNamespaceBlock: boolean = false;
constructor(module: Module) {
super(null, false, null);
this.module = module;
module.getExports().forEach(name => {
const declaration = module.traceExport(name);
if (declaration) {
this.originals[name] = declaration;
}
});
}

addReference(reference: Reference): void {
if (!this.needsNamespaceBlock) {
this.needsNamespaceBlock = true;
}
if (reference.objectPaths.length) {
const ref = reference.objectPaths.shift();
reference.name = ref.name;
reference.start = ref.start;
reference.end = ref.end;
}
values(this.originals).forEach(declaration => {
declaration.addReference(reference);
});
// 必须放在最后执行,因为要将 reference 和当前的 SyntheticNamespaceDeclaration 绑定
super.addReference(reference);
}

renderBlock(intentString = ' ') {
const members = keys(this.originals)
.map((name: string) => {
const originDeclaration = this.originals[name];
return `${intentString}${name}: ${originDeclaration.render()}`;
})
.join(',\n');
return `const ${this.render()} = Object.freeze({\n${members}\n});`;
}

use() {
for (const original of values(this.originals)) {
original.use();
}
}
}

1.9 /src/ast/node.ts

import { Scope } from './scope';
import { Node as ASTNode } from '../../../../ast/rollupAST/src/index';

export interface Node extends ASTNode {
parent?: Node;
_scope?: Scope;
}

1.10 /src/ast/reference.ts

import { Scope } from './scope';
import { Statement } from '../statement';
import { Declaration } from './declaration';

export class Reference {
node: any;
scope: Scope;
statement: Statement;
// declaration 信息在构建依赖图的部分补充
declaration: Declaration | null = null;
name: string;
start: number;
end: number;
objectPaths: any[] = [];
constructor(node: any, scope: Scope, statement: Statement) {
this.node = node;
this.scope = scope;
this.statement = statement;
this.start = node.start;
this.end = node.end;
let root = node;
this.objectPaths = [];
while (root.type === 'MemberExpression') {
this.objectPaths.unshift(root.property);
root = root.object;
}
this.objectPaths.unshift(root);
this.name = root.name;
}
}

1.11 /src/ast/scope.ts

import { keys } from '../utils/object';
import { Statement } from '../statement';
import { Declaration } from './declaration';

interface ScopeOptions {
parent?: Scope;
paramNodes?: any[];
block?: boolean;
statement: Statement;
isTopLevel?: boolean;
}

export class Scope {
parent?: Scope;
paramNodes: any[];
isBlockScope?: boolean;
statement: Statement;
declarations: Record<string, Declaration> = {};
constructor(options: ScopeOptions) {
const { parent, paramNodes, block, statement } = options;
this.parent = parent;
this.paramNodes = paramNodes || [];
this.statement = statement;
this.isBlockScope = !!block;
this.paramNodes.forEach(
(node) =>
(this.declarations[node.name] = new Declaration(
node,
true,
this.statement
))
);
}

addDeclaration(node: any, isBlockDeclaration: boolean) {
// block scope & var, 向上追溯,直到顶层作用域
if (this.isBlockScope && !isBlockDeclaration && this.parent) {
this.parent.addDeclaration(node, isBlockDeclaration);
return;
}
// 变量声明 函数声明
// if (
// node.type === 'VariableDeclaration' ||
// node.type === 'FunctionDeclaration'
// ) {
const key = node.id && node.id.name;
this.declarations[key] = new Declaration(node, false, this.statement);
// }
}

eachDeclaration(fn: (name: string, dec: Declaration) => void) {
keys(this.declarations).forEach((key) => {
fn(key, this.declarations[key]);
});
}

contains(name: string): Declaration {
return this.findDeclaration(name);
}

findDeclaration(name: string): Declaration {
return (
this.declarations[name] ||
(this.parent && this.parent.findDeclaration(name))
);
}
}

1.12 /src/utils/buildScope.ts

import { walk } from 'utils/walk';
import { Scope } from '../ast/scope';
import { Statement } from '../statement';
import {
Node,
NodeType,
FunctionDeclaration,
VariableDeclaration,
VariableDeclarator
} from '../../../../ast/rollupAST/src/index';

export function buildScope(statement: Statement) {
const { node, scope: initialScope } = statement;
let scope = initialScope;
walk(node, {
enter(node: Node) {
// function foo () {...}
if (node.type === NodeType.FunctionDeclaration) {
scope.addDeclaration(node, false);
}
// var let const
if (node.type === NodeType.VariableDeclaration) {
const currentNode = node as VariableDeclaration;
const isBlockDeclaration = currentNode.kind !== 'var';
currentNode.declarations.forEach((declarator: VariableDeclarator) => {
scope.addDeclaration(declarator, isBlockDeclaration);
});
}

let newScope;

// function scope
if (node.type === NodeType.FunctionDeclaration) {
const currentNode = node as FunctionDeclaration;
newScope = new Scope({
parent: scope,
block: false,
paramNodes: currentNode.params,
statement
});
}

// new block state
if (node.type === NodeType.BlockStatement) {
newScope = new Scope({
parent: scope,
block: true,
statement
});
}

if (newScope) {
Object.defineProperty(node, '_scope', {
value: newScope,
configurable: true
});

scope = newScope;
}
},
leave(node: any) {
// 当前 scope 即 node._scope
if (node._scope && scope.parent) {
scope = scope.parent;
}
}
});
}

1.13 /src/utils/findReference.ts

import { walk } from '../utils/walk';
import { Statement } from '../statement';
import { Reference } from '../ast/reference';

function isReference(node: any, parent: any): boolean {
if (node.type === 'MemberExpression' && parent.type !== 'MemberExpression') {
return true;
}
if (node.type === 'Identifier') {
// export { foo as bar }
if (parent.type === 'ExportSpecifier' && node !== parent.local)
return false;
return true;
}
return false;
}

export function findReference(statement: Statement) {
const { references, scope: initialScope, node } = statement;
let scope = initialScope;
walk(node, {
enter(node: any, parent: any) {
if (node._scope) scope = node._scope;
if (isReference(node, parent)) {
const reference = new Reference(node, scope, statement);
references.push(reference);
}
},
leave(node: any) {
if (node._scope && scope.parent) {
scope = scope.parent;
}
}
});
}

1.14 /src/utils/isFunctionDeclaration.ts

import {
Declaration,
ExportDeclaration,
NodeType
} from '../../../../ast/rollupAST/src/index';

export function isFunctionDeclaration(node: Declaration): boolean {
if (!node) return false;

return (
// function foo() {}
node.type === 'FunctionDeclaration' ||
// const foo = function() {}
(node.type === NodeType.VariableDeclarator &&
node.init &&
node.init.type === NodeType.FunctionExpression) ||
// export function ...
// export default function
((node.type === NodeType.ExportNamedDeclaration ||
node.type === NodeType.ExportDefaultDeclaration) &&
!!node.declaration &&
node.declaration.type === NodeType.FunctionDeclaration)
);
}

export function isExportDeclaration(node: ExportDeclaration): boolean {
return /^Export/.test(node.type);
}

export function isImportDeclaration(node: any) {
return node.type === 'ImportDeclaration';
}

1.15 /src/utils/makeLegalIdentifier.ts

const reservedWords =
'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public'.split(
' '
);
const builtins =
'Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl'.split(
' '
);

export default function makeLegalIdentifier(str: string) {
str = str
.replace(/-(\w)/g, (_, letter) => letter.toUpperCase())
.replace(/[^$_a-zA-Z0-9]/g, '_');
if (reservedWords.concat(builtins).includes(str)) {
str = `_${str}`;
}
return str;
}

1.16 /src/utils/object.ts

export const keys = Object.keys;
export const values = Object.values;

export const hasOwnProp = Object.prototype.hasOwnProperty;

export function has(obj: any, prop: string) {
return hasOwnProp.call(obj, prop);
}

1.17 /src/utils/resolve.ts

import { dirname, isAbsolute, resolve, extname } from 'path';

export function removeExtension(p: string) {
return p.replace(extname(p), '');
}

export function defaultResolver(id: string, importer: string | null) {
// absolute paths are left untouched
if (isAbsolute(id)) return id;

// external modules stay external
if (!id.startsWith('.')) return false;

const resolvedPath = importer ? resolve(dirname(importer), id) : resolve(id);
return resolvedPath;
}

1.18 /src/utils/walk.ts

let shouldSkip;
let shouldAbort: boolean;

export function walk(ast: any, { enter, leave }: { enter: any; leave: any }) {
shouldAbort = false;
visit(ast, null, enter, leave);
}

let context = {
skip: () => (shouldSkip = true),
abort: () => (shouldAbort = true)
};

let childKeys = {} as Record<string, string[]>;

let toString = Object.prototype.toString;

function isArray(thing: Object) {
return toString.call(thing) === '[object Array]';
}

function visit(node: any, parent: any, enter: any, leave: any, prop?: string) {
if (!node || shouldAbort) return;

if (enter) {
shouldSkip = false;
enter.call(context, node, parent, prop);
if (shouldSkip || shouldAbort) return;
}

let keys =
childKeys[node.type] ||
(childKeys[node.type] = Object.keys(node).filter(
key => typeof node[key] === 'object'
));

let key, value;

for (let i = 0; i < keys.length; i++) {
key = keys[i];
value = node[key];

if (isArray(value)) {
for (let j = 0; j < value.length; j++) {
visit(value[j], node, enter, leave, key);
}
} else if (value && value.type) {
visit(value, node, enter, leave, key);
}
}

if (leave && !shouldAbort) {
leave(node, parent, prop);
}
}

二、测试


2.1 /test/dep1.js

const a = 1;

export const b = a + 1;

export const multi = function (a, b) {
return a * b;
};

export function testFunc() {
console.log(a);
}

export default function (a, b) {
return a + b;
}

2.2 /test/dep2.js

import afunc from './dep1.js';

const a = 1;

export const c = a + 2;

export const log = function () {
afunc(1, 2);
console.log(123);
};

2.3 /test/dep3.js

export default function () {
console.log(1);
}

2.4 /test/index.js

import * as dep1 from './dep1.js';
import { log, c } from './dep2.js';
import testDefaultFunc from './dep3.js';

export const cc = dep1.b;
export const bb = testDefaultFunc();
export const aa = dep1.testFunc();

export default dep1.multi;

2.5 /test/test.js

const fs = require('fs');
const path = require('path');
const { rollup } = require('../dist/rollup');

const resolve = p => {
return path.resolve(__dirname, p);
};

async function build() {
const bundle = await rollup({
input: resolve('./index.js')
});
const res = bundle.generate();
fs.writeFileSync(resolve('./bundle.js'), res.code);
}

build();