Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"extends": "airbnb-base", | ||
"rules": { | ||
"arrow-body-style": "off", | ||
"consistent-return": "off", | ||
"class-methods-use-this": "off", | ||
"global-require": "off", | ||
"import/no-dynamic-require": "off", | ||
"import/prefer-default-export": "off", | ||
"no-plusplus": "off", | ||
"no-confusing-arrow": "off", | ||
"no-console": "off", | ||
"no-multi-assign": "off", | ||
"no-param-reassign": "off", | ||
"no-return-assign": "off", | ||
"no-underscore-dangle": "off", | ||
"no-unused-expressions": "off", | ||
"no-use-before-define": "off" | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
node_modules | ||
npm-debug.log | ||
npm-debug.log* | ||
.DS_Store | ||
yarn-error.log* | ||
yarn.lock | ||
package-lock.json | ||
.vscode |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
## 馃殌 Minipack | ||
|
||
> An ultra-simplified example of a modern module bundler written in JavaScript | ||
### Development | ||
|
||
Start by installing dependencies: | ||
|
||
```sh | ||
$ npm install | ||
``` | ||
|
||
And then run our script: | ||
|
||
```sh | ||
$ node run.js | ||
``` | ||
|
||
### FAQ | ||
|
||
#### What's in it for me? | ||
|
||
Most of us don't have to think too much about module bundlers in our day jobs. However, module bundlers are all around us, and if you're a web developer you are probably using module bundlers on a daily basis. | ||
|
||
Having a good understanding of how popular tools like [Webpack](https://github.com/webpack/webpack) or [Browserify](https://github.com/browserify/browserify) work is extremely helpful. | ||
|
||
The purpose of this example is to help you understand how *most* module bundlers work, and that they're not as scary as you might think. | ||
|
||
#### Awesome, where do I start? | ||
|
||
Head on to the source code: [src/minipack.js](src/minipack.js). | ||
|
||
### Additional links | ||
|
||
- [AST Explorer](https://astexplorer.net/) | ||
- [Babel REPL](https://babeljs.io/repl/) | ||
- [Babylon](https://github.com/babel/babel/tree/master/packages/babylon) | ||
- [Babel Plugin Handbook](https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md) | ||
- [Webpack: dependency managment](https://webpack.js.org/guides/dependency-management) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import message from './message'; | ||
|
||
console.log(message); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { name } from './name'; | ||
|
||
export default `hello ${name}!`; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const name = 'world'; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "minipack", | ||
"version": "1.0.0", | ||
"description": "", | ||
"author": "Ronen Amiel", | ||
"license": "MIT", | ||
"dependencies": { | ||
"babel-core": "^6.26.0", | ||
"babel-preset-es2015": "^6.24.1", | ||
"babel-traverse": "^6.26.0", | ||
"babylon": "^6.18.0", | ||
"eslint": "^4.17.0", | ||
"eslint-config-airbnb-base": "^12.1.0", | ||
"eslint-plugin-import": "^2.8.0" | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const minipack = require('./src/minipack'); | ||
|
||
const entry = require.resolve('./example/entry'); | ||
const result = minipack(entry); | ||
|
||
console.log(result); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
/** | ||
* Back in the day, it was enough to just concatenate scripts together. That approach had problems: | ||
* scripts had to be concatenated in the correct order, advanced techniques like hot module | ||
* replacement or tree shaking required better understanding of the project. | ||
* | ||
* Module bundlers take a different approach, they treat your project as a dependency graph. | ||
* | ||
* Our module bundler will processes our application: it will start from an entry file, this file | ||
* is the root of our application. It will recursively builds a dependency graph that includes every | ||
* module our application needs, and finally it will package all of those modules into just one | ||
* bundle to be loaded by the browser. | ||
*/ | ||
|
||
/** | ||
* Please note: this is an ultra-simplified example. Error handling or handling edge cases like | ||
* circular dependencies, caching module exports, parsing each module just once or others are | ||
* skipped to make this example as simple as possible. | ||
*/ | ||
|
||
const fs = require('fs'); | ||
const path = require('path'); | ||
const babylon = require('babylon'); | ||
const traverse = require('babel-traverse').default; | ||
const { transformFromAst } = require('babel-core'); | ||
const { resolveFrom } = require('./utils'); | ||
|
||
let id = 0; | ||
|
||
// We start by defining a recursive function to create our dependency graph. This graph will be | ||
// represented as an object with a files absolute paths as keys and an object with that file's data | ||
// as its values. | ||
function createDependencyGraph(filename) { | ||
// We start by reading the file's content as a string | ||
const content = fs.readFileSync(filename, 'utf-8'); | ||
|
||
// We then parse it with a JavaScript parser (see https://astexplorer.net) and generate an AST. | ||
const ast = babylon.parse(content, { | ||
sourceType: 'module', | ||
}); | ||
|
||
// This array will hold the relative paths of modules this module depends on. | ||
const dependencies = []; | ||
|
||
// We traverse the AST to try and understand which modules this module depends on. | ||
traverse(ast, { | ||
// ES6 modules are fairly easy because they are static. This means that you can't import a | ||
// variable, or conditionaly import another module. Every time we see an import statement we | ||
// can just count it as a dependency. | ||
ImportDeclaration({ node }) { | ||
dependencies.push(node.source.value); | ||
}, | ||
// Covering require calls is a bit trickier: We'll go over every function call in this file, | ||
// if its name is 'require', and its only argument is a string then we'll count it in as | ||
// a dependency. It won't cover all the cases, but it's good enough. | ||
// | ||
// Tools like Webpack can actually handle some pretty rough cases, see: https://webpack.js.org/guides/dependency-management | ||
CallExpression({ node }) { | ||
const isRequire = node.callee.name === 'require' && | ||
node.arguments.length === 1 && | ||
node.arguments[0].type === 'StringLiteral'; | ||
|
||
if (isRequire) { | ||
dependencies.push(node.arguments[0].value); | ||
} | ||
}, | ||
}); | ||
|
||
// We define an object indexed by our file's absolute path that contains all of the information we | ||
// have about our file. | ||
const initial = { | ||
[filename]: { | ||
// We define an id to have a shorter identifier to a file other than its absolute file path. | ||
id: id++, | ||
ast, | ||
content, | ||
// We represent this module's list of dependencies as an object with relative paths as keys | ||
// and absolute paths as values | ||
dependencies: dependencies.reduce((acc, relativeFilename) => { | ||
const fullFilename = resolveFrom(path.dirname(filename), relativeFilename); | ||
|
||
return { | ||
...acc, | ||
[relativeFilename]: fullFilename, | ||
}; | ||
}, {}), | ||
}, | ||
}; | ||
|
||
// We iterate over all of this module's dependencies and extract their dependencies, merging it | ||
// all into one big object which we return. | ||
return Object.values(initial[filename].dependencies) | ||
.reduce((acc, fullFilename) => { | ||
const result = createDependencyGraph(fullFilename); | ||
|
||
return { | ||
...acc, | ||
...result, | ||
}; | ||
}, initial); | ||
} | ||
|
||
// This is the main function we export: it takes an entry point and return our bundled application | ||
// as one large string. That string can later be saved as a JavaScript file. | ||
module.exports = (entry) => { | ||
// We start by creating our graph. Remember, this is just a flat object with its keys being | ||
// absolute file paths and its values are objects with data on those modules. | ||
const graph = createDependencyGraph(entry); | ||
|
||
// We used import and export statements and other features that may not be supported in all | ||
// web browsers. | ||
// | ||
// To make sure our bundle runs in a browser we will transpile it with Babel (see https://babeljs.io) | ||
// | ||
// We iterate on all the objects in our graph, transpile their code to EcmaScript 5 and mutate | ||
// them by adding a new 'code' property to them. It will hold the value of their transpiled code | ||
// as a string. | ||
Object.values(graph).forEach((asset) => { | ||
// We use transformFromAst instead of the regular transform to save computing power and make | ||
// bundling faster. | ||
const { code } = transformFromAst(asset.ast, asset.content, { | ||
presets: ['es2015'], | ||
}); | ||
|
||
// We add our result as a new property. | ||
asset.code = code; | ||
}); | ||
|
||
/** | ||
* We're done creating our dependency graph and going over it to transpile each module with Babel. | ||
* Now we're going to package it all into one bundle. | ||
* | ||
* The bundle should contain each module in our graph within its own scope. That means that | ||
* defining a variable in one module shouldn't affect others in the bundle. | ||
* | ||
* Our transpiled module modules use the commonjs module system: they expect a global require | ||
* function to be available along with a global module and an exports objects. Those functions | ||
* and objects are not normally available in a browser, so we'll have to implement them in our | ||
* bundle. | ||
* | ||
* Our bundle will contain one self invoking function that will accept one argument: an object | ||
* with data about our modules. It should have module IDs as keys and a tuple (an array with two | ||
* values) for values. | ||
* | ||
* The first value in the array will be a function to wrap the code of that module to create a | ||
* scope for it. It will also accept a require function, a module object and an exports object | ||
* that our module expects to be available globally. | ||
* | ||
* Our function will then create an implementation of the require function and require the entry | ||
* module to fire up the application. | ||
*/ | ||
|
||
// We start by defining the object to be fed to our self invoking function. | ||
let modules = ''; | ||
|
||
// We iterate the graph | ||
Object.values(graph).forEach((mod) => { | ||
// The second value in the array is an a mapping object. This object will contain the relative | ||
// path of every module this module depends on and its absolute ID as a value. | ||
// | ||
// In our implementation of the require function we'll need to resolve relative module paths | ||
// for every one of our modules. | ||
const mapping = Object.keys(mod.dependencies).reduce((acc, relativeFilename) => { | ||
const fullFilename = mod.dependencies[relativeFilename]; | ||
|
||
return { | ||
...acc, | ||
[relativeFilename]: graph[fullFilename].id, | ||
}; | ||
}, {}); | ||
|
||
// For every module in the graph we create a new index in our object: an array containing | ||
// the wrapping function for our module code and its mappings object. | ||
modules += ` | ||
${mod.id}: [ | ||
function (require, module, exports) { ${mod.code} }, | ||
${JSON.stringify(mapping)} | ||
],`; | ||
}); | ||
|
||
/** | ||
* We create a simple implementation of the require function: it accepts a module ID and looks for | ||
* it in the modules object. Our modules expect their require function to take a relative path to | ||
* a module instead of an ID. Also, that relative path should be relative to the requiring module, | ||
* which may be different for each module. | ||
* | ||
* To handle that, whenever a module is being required we will create a new require function, | ||
* namely localRequire to map the relative module paths to module IDs using our previous mapping. | ||
* | ||
* We feed the wrapping module function its own localRequire, along with a module.exports object | ||
* for it to mutate, and eventually return it. | ||
*/ | ||
const bundle = ` | ||
(function(modules) { | ||
function require(id) { | ||
const [module, mapping] = modules[id]; | ||
function localRequire(name) { | ||
return require(mapping[name]); | ||
} | ||
const localModule = { exports: {} }; | ||
module(localRequire, localModule, localModule.exports); | ||
return localModule.exports; | ||
} | ||
require(0); | ||
})({${modules}}) | ||
`; | ||
|
||
// We simply return the result, you made it! :) | ||
return bundle; | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const path = require('path'); | ||
|
||
module.exports.resolveFrom = (context, filename) => { | ||
return require.resolve(path.resolve(context, filename)); | ||
}; |