/** * Module bundlers compile small pieces of code into something larger and more * complex that can run in a web browser. These small pieces are just JavaScript * files, and dependencies between them are expressed by a module system * (https://webpack.js.org/concepts/modules). * * Module bundlers have this concept of an entry file. Instead of adding a few * script tags in the browser and letting them run, we let the bundler know * which file is the main file of our application. This is the file that should * bootstrap our entire application. * * Our bundler will start from that entry file, and it will try to understand * which files it depends on. Then, it will try to understand which files its * dependencies depend on. It will keep doing that until it figures out about * every module in our application, and how they depend on one another. * * This understanding of a project is called the dependency graph. * * In this example, we will create a dependency graph and use it to package * all of its modules in one bundle. * * Let's begin :) * * Please note: This is a very simplified example. Handling cases such as * circular dependencies, caching module exports, parsing each module just once * and 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'); let ID = 0; // We start by creating a function that will accept a path to a file, read // its contents, and extract its dependencies. function createAsset(filename) { // Read the content of the file as a string. const content = fs.readFileSync(filename, 'utf-8'); // Now we try to figure out which files this file depends on. We can do that // by looking at its content for import strings. However, this is a pretty // clunky approach, so instead, we will use a JavaScript parser. // // JavaScript parsers are tools that can read and understand JavaScript code. // They generate a more abstract model called an AST (abstract syntax tree). // I strongly suggest that you look at AST Explorer (https://astexplorer.net) // to see how an AST looks like. // // The AST contains a lot of information about our code. We can query it to // understand what our code is trying to do. 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. To do that, we check every import declaration in the AST. traverse(ast, { // EcmaScript modules are fairly easy because they are static. This means // that you can't import a variable, or conditionally import another module. // Every time we see an import statement we can just count its value as a // dependency. ImportDeclaration: ({node}) => { // We push the value that we import into the dependencies array. dependencies.push(node.source.value); }, }); // We also assign a unique identifier to this module by incrementing a simple // counter. const id = ID++; // We use EcmaScript modules and other JavaScript features that may not be // supported on all browsers. To make sure our bundle runs in all browsers we // will transpile it with Babel (see https://babeljs.io). // // The `presets` option is a set of rules that tell Babel how to transpile // our code. We use `babel-preset-env` to transpile our code to something // that most browsers can run. const {code} = transformFromAst(ast, null, { presets: ['env'], }); // Return all information about this module. return { id, filename, dependencies, code, }; } // Now that we can extract the dependencies of a single module, we are going to // start by extracting the dependencies of the entry file. // // Then, we are going to extract the dependencies of every one of its // dependencies. We will keep that going until we figure out about every module // in the application and how they depend on one another. This understanding of // a project is called the dependency graph. function createGraph(entry) { // Start by parsing the entry file. const mainAsset = createAsset(entry); // We're going to use a queue to parse the dependencies of every asset. To do // that we are defining an array with just the entry asset. const queue = [mainAsset]; // We use a `for ... of` loop to iterate over the queue. Initially the queue // only has one asset but as we iterate it we will push additional new assets // into the queue. This loop will terminate when the queue is empty. for (const asset of queue) { // Every one of our assets has a list of relative paths to the modules it // depends on. We are going to iterate over them, parse them with our // `createAsset()` function, and track the dependencies this module has in // this object. asset.mapping = {}; // This is the directory this module is in. const dirname = path.dirname(asset.filename); // We iterate over the list of relative paths to its dependencies. asset.dependencies.forEach(relativePath => { // Our `createAsset()` function expects an absolute filename. The // dependencies array is an array of relative paths. These paths are // relative to the file that imported them. We can turn the relative path // into an absolute one by joining it with the path to the directory of // the parent asset. const absolutePath = path.join(dirname, relativePath); // Parse the asset, read its content, and extract its dependencies. const child = createAsset(absolutePath); // It's essential for us to know that `asset` depends on `child`. We // express that relationship by adding a new property to the `mapping` // object with the id of the child. asset.mapping[relativePath] = child.id; // Finally, we push the child asset into the queue so its dependencies // will also be iterated over and parsed. queue.push(child); }); } // At this point the queue is just an array with every module in the target // application: This is how we represent our graph. return queue; } // Next, we define a function that will use our graph and return a bundle that // we can run in the browser. // // Our bundle will have just one self-invoking function: // // (function() {})() // // That function will receive just one parameter: An object with information // about every module in our graph. function bundle(graph) { let modules = ''; // Before we get to the body of that function, we'll construct the object that // we'll pass to it as a parameter. Please note that this string that we're // building gets wrapped by two curly braces ({}) so for every module, we add // a string of this format: `key: value,`. graph.forEach(mod => { // Every module in the graph has an entry in this object. We use the // module's id as the key and an array for the value (we have 2 values for // every module). // // The first value is the code of each module wrapped with a function. This // is because modules should be scoped: Defining a variable in one module // shouldn't affect others or the global scope. // // Our modules, after we transpiled them, use the CommonJS module system: // They expect a `require`, a `module` and an `exports` objects to be // available. Those are not normally available in the browser so we'll // implement them and inject them into our function wrappers. // // For the second value, we stringify the mapping between a module and its // dependencies. This is an object that looks like this: // { './relative/path': 1 }. // // This is because the transpiled code of our modules has calls to // `require()` with relative paths. When this function is called, we should // be able to know which module in the graph corresponds to that relative // path for this module. modules += `${mod.id}: [ function (require, module, exports) { ${mod.code} }, ${JSON.stringify(mod.mapping)}, ],`; }); // Finally, we implement the body of the self-invoking function. // // We start by creating a `require()` function: It accepts a module id and // looks for it in the `modules` object we constructed previously. We // destructure over the two-value array to get our function wrapper and the // mapping object. // // The code of our modules has calls to `require()` with relative file paths // instead of module ids. Our require function expects module ids. Also, two // modules might `require()` the same relative path but mean two different // modules. // // To handle that, when a module is required we create a new, dedicated // `require` function for it to use. It will be specific to that module and // will know to turn its relative paths into ids by using the module's // mapping object. The mapping object is exactly that, a mapping between // relative paths and module ids for that specific module. // // Lastly, with CommonJs, when a module is required, it can expose values by // mutating its `exports` object. The `exports` object, after it has been // changed by the module's code, is returned from the `require()` function. const result = ` (function(modules) { function require(id) { const [fn, mapping] = modules[id]; function localRequire(name) { return require(mapping[name]); } const module = { exports : {} }; fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}}) `; // We simply return the result, hurray! :) return result; } const graph = createGraph('./example/entry.js'); const result = bundle(graph); console.log(result);
来源:https://github.com/ronami/minipack