Last active
June 6, 2020 09:17
-
-
Save mizchi/368c75fc1088dabfda7dfed726ef9f85 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// small bundler | |
import path from "path"; | |
import { parse } from "@babel/parser"; | |
import traverse from "@babel/traverse"; | |
import generate from "@babel/generator"; | |
import * as t from "@babel/types"; | |
import type { IPromisesAPI } from "memfs/lib/promises"; | |
import createFs from "memfs/lib/promises"; | |
import { vol } from "memfs"; | |
// helper | |
function createMemoryFs(files: { [k: string]: string }): IPromisesAPI { | |
vol.fromJSON(files, "/"); | |
return createFs(vol) as IPromisesAPI; | |
} | |
type Module = { | |
ast: t.File; | |
filepath: string; | |
imports: Import[]; | |
}; | |
type Import = { | |
filepath: string; | |
}; | |
type Output = { | |
filepath: string; | |
code: string; | |
imports: Import[]; | |
}; | |
class Bundler { | |
public modulesMap = new Map<string, Module>(); | |
public outModules: Array<Output> = []; | |
fs: IPromisesAPI; | |
constructor(public files: { [k: string]: string }) { | |
this.fs = createMemoryFs(files); | |
} | |
public async bundle(entry: string) { | |
await this.addModule(entry); | |
await this.transform(entry); | |
return await this.emit("/index.js"); | |
} | |
async addModule(filepath: string) { | |
if (this.modulesMap.has(filepath)) { | |
return; | |
} | |
const basepath = path.dirname(filepath); | |
const code = (await this.fs.readFile(filepath, { | |
encoding: "utf-8", | |
})) as string; | |
const ast = parse(code, { | |
sourceFilename: filepath, | |
sourceType: "module", | |
}); | |
let imports: Import[] = []; | |
traverse(ast, { | |
ImportDeclaration(nodePath) { | |
const target = nodePath.node.source.value; | |
const absPath = path.join(basepath, target); | |
imports.push({ | |
filepath: absPath, | |
}); | |
}, | |
}); | |
await Promise.all( | |
imports.map((imp) => { | |
return this.addModule(imp.filepath); | |
}) | |
); | |
this.modulesMap.set(filepath, { | |
filepath, | |
ast, | |
imports, | |
}); | |
} | |
async transform(filepath: string) { | |
const mod = this.modulesMap.get(filepath)!; | |
const alreadyIncluded = this.outModules.find( | |
(m) => m.filepath === filepath | |
); | |
if (alreadyIncluded) { | |
return; | |
} | |
const basepath = path.dirname(filepath); | |
const newImportStmts: t.Statement[] = []; | |
traverse(mod.ast, { | |
ImportDeclaration(nodePath) { | |
const target = nodePath.node.source.value; | |
const absPath = path.join(basepath, target); | |
const names: [string, string][] = []; | |
nodePath.node.specifiers.forEach((n) => { | |
if (n.type === "ImportDefaultSpecifier") { | |
names.push(["default", n.local.name]); | |
} | |
if (n.type === "ImportSpecifier") { | |
names.push([n.imported.name, n.local.name]); | |
} | |
if (n.type === "ImportNamespaceSpecifier") { | |
newImportStmts.push( | |
t.variableDeclaration("const", [ | |
t.variableDeclarator( | |
t.identifier(n.local.name), | |
t.callExpression(t.identifier("$$import"), [ | |
t.stringLiteral(absPath), | |
]) | |
), | |
]) | |
); | |
} | |
}); | |
const newNode = t.variableDeclaration("const", [ | |
t.variableDeclarator( | |
t.objectPattern( | |
names.map(([imported, local]) => { | |
return t.objectProperty( | |
t.identifier(imported), | |
t.identifier(local) | |
); | |
}) | |
), | |
t.callExpression(t.identifier("$$import"), [ | |
t.stringLiteral(absPath), | |
]) | |
), | |
]); | |
newImportStmts.push(newNode); | |
nodePath.replaceWith(t.emptyStatement()); | |
}, | |
ExportDefaultDeclaration(nodePath) { | |
const name = "default"; | |
const right = nodePath.node.declaration as any; | |
const newNode = t.expressionStatement( | |
t.assignmentExpression( | |
"=", | |
t.memberExpression( | |
t.identifier("$$exports"), | |
t.stringLiteral(name), | |
true | |
), | |
right | |
) | |
); | |
nodePath.replaceWith(newNode); | |
}, | |
ExportNamedDeclaration(nodePath) { | |
// TODO: name mapping | |
// TODO: Export multiple name | |
if (nodePath.node.declaration) { | |
const decl = nodePath.node.declaration.declarations[0]; | |
const name = decl.id.name; | |
const right = decl.init; | |
const newNode = t.expressionStatement( | |
t.assignmentExpression( | |
"=", | |
t.memberExpression( | |
t.identifier("$$exports"), | |
t.identifier(name) | |
// true | |
), | |
right | |
) | |
); | |
nodePath.replaceWith(newNode); | |
} else { | |
// export { a as b } | |
const exportNames: Array<{ exported: string; imported: string }> = []; | |
for (const specifier of nodePath.node.specifiers) { | |
if (specifier.type == "ExportSpecifier") { | |
exportNames.push({ | |
exported: specifier.exported.name, | |
imported: specifier.local.name, | |
}); | |
} | |
} | |
nodePath.replaceWith( | |
t.blockStatement( | |
exportNames.map((exp) => { | |
return t.expressionStatement( | |
t.assignmentExpression( | |
"=", | |
t.memberExpression( | |
t.identifier("$$exports"), | |
t.identifier(exp.exported) | |
// true | |
), | |
nodePath.node.source | |
? t.memberExpression( | |
t.callExpression(t.identifier("$$import"), [ | |
t.stringLiteral( | |
path.join(basepath, nodePath.node.source.value) | |
), | |
]), | |
t.identifier(exp.imported) | |
) | |
: t.identifier(exp.imported) | |
) | |
); | |
}) | |
) | |
); | |
} | |
}, | |
}); | |
const out = { | |
...mod.ast, | |
program: { | |
...mod.ast.program, | |
body: [...newImportStmts, ...mod.ast.program.body], | |
}, | |
}; | |
const gen = generate(out); | |
this.outModules.push({ | |
imports: mod.imports, | |
filepath: mod.filepath, | |
code: gen.code, | |
}); | |
await Promise.all( | |
mod.imports.map((imp) => { | |
return this.transform(imp.filepath); | |
}) | |
); | |
} | |
async emit(entry: string) { | |
const entryMod = this.outModules.find((m) => m.filepath === entry); | |
const importCodes = this.outModules | |
.filter((m) => m.filepath !== entry) | |
.map((m) => { | |
return `$$import("${m.filepath}");`; | |
}) | |
.join("\n"); | |
const mods = this.outModules | |
.filter((m) => m.filepath !== entry) | |
.map((m) => { | |
return `"${m.filepath}": ($$exports) => { | |
${m.code} | |
return $$exports; | |
} | |
`; | |
}) | |
.join(","); | |
return `// minibundle generate | |
const $$exported = {}; | |
const $$modules = { ${mods} }; | |
function $$import(id){ | |
if ($$exported[id]) { | |
return $$exported[id]; | |
} | |
$$exported[id] = {}; | |
$$modules[id]($$exported[id]); | |
return $$exported[id]; | |
} | |
// evaluate as static module | |
${importCodes}; | |
// -- runner -- | |
const $$exports = {}; // dummy | |
${entryMod?.code}; | |
`; | |
} | |
} | |
// runtime | |
const files = { | |
"/bar.js": ` | |
import foo from "./foo.js"; | |
console.log("eval bar once") | |
export default "bar$" + foo | |
`, | |
"/foo.js": ` | |
console.log("eval foo once") | |
export default "foo$default"; | |
export const b = "b"; | |
export const a = "a"; | |
`, | |
"/index.js": ` | |
import * as t from "./foo.js"; | |
import foo, {a, b as c} from "./foo.js"; | |
import bar from "./bar.js"; | |
export const x = c; | |
export default 1; | |
console.log(foo, bar); | |
`, | |
}; | |
// @ts-ignore | |
import prettier from "prettier"; | |
async function main() { | |
const bundler = new Bundler(files); | |
const built = await bundler.bundle("/index.js"); | |
console.log(prettier.format(built)); | |
console.log("--- eval ----"); | |
eval(built); | |
} | |
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// minibundle generate | |
const $$exported = {}; | |
const $$modules = { | |
"/foo.js": ($$exports) => { | |
console.log("eval foo once"); | |
$$exports["default"] = "foo$default"; | |
$$exports["b"] = "b"; | |
$$exports["a"] = "a"; | |
return $$exports; | |
}, | |
"/bar.js": ($$exports) => { | |
const { default: foo } = $$import("/foo.js"); | |
console.log("eval bar once"); | |
$$exports["default"] = "bar$" + foo; | |
return $$exports; | |
}, | |
}; | |
function $$import(id) { | |
if ($$exported[id]) { | |
return $$exported[id]; | |
} | |
$$exported[id] = {}; | |
$$modules[id]($$exported[id]); | |
return $$exported[id]; | |
} | |
// additional code | |
globalThis.$$import = $$import; | |
// evaluate as static module | |
$$import("/foo.js"); | |
$$import("/bar.js"); | |
// -- runner -- | |
const $$exports = {}; // dummy | |
const { default: foo, a: a, b: c } = $$import("/foo.js"); | |
const { default: bar } = $$import("/bar.js"); | |
$$exports["x"] = c; | |
$$exports["default"] = 1; | |
console.log(foo, bar); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment