1. /**
  2. * Module bundlers compile small pieces of code into something larger and more
  3. * complex that can run in a web browser. These small pieces are just JavaScript
  4. * files, and dependencies between them are expressed by a module system
  5. * (https://webpack.js.org/concepts/modules).
  6. *
  7. * Module bundlers have this concept of an entry file. Instead of adding a few
  8. * script tags in the browser and letting them run, we let the bundler know
  9. * which file is the main file of our application. This is the file that should
  10. * bootstrap our entire application.
  11. *
  12. * Our bundler will start from that entry file, and it will try to understand
  13. * which files it depends on. Then, it will try to understand which files its
  14. * dependencies depend on. It will keep doing that until it figures out about
  15. * every module in our application, and how they depend on one another.
  16. *
  17. * This understanding of a project is called the dependency graph.
  18. *
  19. * In this example, we will create a dependency graph and use it to package
  20. * all of its modules in one bundle.
  21. *
  22. * Let's begin :)
  23. *
  24. * Please note: This is a very simplified example. Handling cases such as
  25. * circular dependencies, caching module exports, parsing each module just once
  26. * and others are skipped to make this example as simple as possible.
  27. */
  28.  
  29. const fs = require('fs');
  30. const path = require('path');
  31. const babylon = require('babylon');
  32. const traverse = require('babel-traverse').default;
  33. const {transformFromAst} = require('babel-core');
  34.  
  35. let ID = 0;
  36.  
  37. // We start by creating a function that will accept a path to a file, read
  38. // its contents, and extract its dependencies.
  39. function createAsset(filename) {
  40. // Read the content of the file as a string.
  41. const content = fs.readFileSync(filename, 'utf-8');
  42.  
  43. // Now we try to figure out which files this file depends on. We can do that
  44. // by looking at its content for import strings. However, this is a pretty
  45. // clunky approach, so instead, we will use a JavaScript parser.
  46. //
  47. // JavaScript parsers are tools that can read and understand JavaScript code.
  48. // They generate a more abstract model called an AST (abstract syntax tree).
  49.  
  50. // I strongly suggest that you look at AST Explorer (https://astexplorer.net)
  51. // to see how an AST looks like.
  52. //
  53. // The AST contains a lot of information about our code. We can query it to
  54. // understand what our code is trying to do.
  55. const ast = babylon.parse(content, {
  56. sourceType: 'module',
  57. });
  58.  
  59. // This array will hold the relative paths of modules this module depends on.
  60. const dependencies = [];
  61.  
  62. // We traverse the AST to try and understand which modules this module depends
  63. // on. To do that, we check every import declaration in the AST.
  64. traverse(ast, {
  65. // EcmaScript modules are fairly easy because they are static. This means
  66. // that you can't import a variable, or conditionally import another module.
  67. // Every time we see an import statement we can just count its value as a
  68. // dependency.
  69. ImportDeclaration: ({node}) => {
  70. // We push the value that we import into the dependencies array.
  71. dependencies.push(node.source.value);
  72. },
  73. });
  74.  
  75. // We also assign a unique identifier to this module by incrementing a simple
  76. // counter.
  77. const id = ID++;
  78.  
  79. // We use EcmaScript modules and other JavaScript features that may not be
  80. // supported on all browsers. To make sure our bundle runs in all browsers we
  81. // will transpile it with Babel (see https://babeljs.io).
  82. //
  83. // The `presets` option is a set of rules that tell Babel how to transpile
  84. // our code. We use `babel-preset-env` to transpile our code to something
  85. // that most browsers can run.
  86. const {code} = transformFromAst(ast, null, {
  87. presets: ['env'],
  88. });
  89.  
  90. // Return all information about this module.
  91. return {
  92. id,
  93. filename,
  94. dependencies,
  95. code,
  96. };
  97. }
  98.  
  99. // Now that we can extract the dependencies of a single module, we are going to
  100. // start by extracting the dependencies of the entry file.
  101. //
  102. // Then, we are going to extract the dependencies of every one of its
  103. // dependencies. We will keep that going until we figure out about every module
  104. // in the application and how they depend on one another. This understanding of
  105. // a project is called the dependency graph.
  106. function createGraph(entry) {
  107. // Start by parsing the entry file.
  108. const mainAsset = createAsset(entry);
  109.  
  110. // We're going to use a queue to parse the dependencies of every asset. To do
  111. // that we are defining an array with just the entry asset.
  112. const queue = [mainAsset];
  113.  
  114. // We use a `for ... of` loop to iterate over the queue. Initially the queue
  115. // only has one asset but as we iterate it we will push additional new assets
  116. // into the queue. This loop will terminate when the queue is empty.
  117. for (const asset of queue) {
  118. // Every one of our assets has a list of relative paths to the modules it
  119. // depends on. We are going to iterate over them, parse them with our
  120. // `createAsset()` function, and track the dependencies this module has in
  121. // this object.
  122. asset.mapping = {};
  123.  
  124. // This is the directory this module is in.
  125. const dirname = path.dirname(asset.filename);
  126.  
  127. // We iterate over the list of relative paths to its dependencies.
  128. asset.dependencies.forEach(relativePath => {
  129. // Our `createAsset()` function expects an absolute filename. The
  130. // dependencies array is an array of relative paths. These paths are
  131. // relative to the file that imported them. We can turn the relative path
  132. // into an absolute one by joining it with the path to the directory of
  133. // the parent asset.
  134. const absolutePath = path.join(dirname, relativePath);
  135.  
  136. // Parse the asset, read its content, and extract its dependencies.
  137. const child = createAsset(absolutePath);
  138.  
  139. // It's essential for us to know that `asset` depends on `child`. We
  140. // express that relationship by adding a new property to the `mapping`
  141. // object with the id of the child.
  142. asset.mapping[relativePath] = child.id;
  143.  
  144. // Finally, we push the child asset into the queue so its dependencies
  145. // will also be iterated over and parsed.
  146. queue.push(child);
  147. });
  148. }
  149.  
  150. // At this point the queue is just an array with every module in the target
  151. // application: This is how we represent our graph.
  152. return queue;
  153. }
  154.  
  155. // Next, we define a function that will use our graph and return a bundle that
  156. // we can run in the browser.
  157. //
  158. // Our bundle will have just one self-invoking function:
  159. //
  160. // (function() {})()
  161. //
  162. // That function will receive just one parameter: An object with information
  163. // about every module in our graph.
  164. function bundle(graph) {
  165. let modules = '';
  166.  
  167. // Before we get to the body of that function, we'll construct the object that
  168. // we'll pass to it as a parameter. Please note that this string that we're
  169. // building gets wrapped by two curly braces ({}) so for every module, we add
  170. // a string of this format: `key: value,`.
  171. graph.forEach(mod => {
  172. // Every module in the graph has an entry in this object. We use the
  173. // module's id as the key and an array for the value (we have 2 values for
  174. // every module).
  175. //
  176. // The first value is the code of each module wrapped with a function. This
  177. // is because modules should be scoped: Defining a variable in one module
  178. // shouldn't affect others or the global scope.
  179. //
  180. // Our modules, after we transpiled them, use the CommonJS module system:
  181. // They expect a `require`, a `module` and an `exports` objects to be
  182. // available. Those are not normally available in the browser so we'll
  183. // implement them and inject them into our function wrappers.
  184. //
  185. // For the second value, we stringify the mapping between a module and its
  186. // dependencies. This is an object that looks like this:
  187. // { './relative/path': 1 }.
  188. //
  189. // This is because the transpiled code of our modules has calls to
  190. // `require()` with relative paths. When this function is called, we should
  191. // be able to know which module in the graph corresponds to that relative
  192. // path for this module.
  193. modules += `${mod.id}: [
  194. function (require, module, exports) {
  195. ${mod.code}
  196. },
  197. ${JSON.stringify(mod.mapping)},
  198. ],`;
  199. });
  200.  
  201. // Finally, we implement the body of the self-invoking function.
  202. //
  203. // We start by creating a `require()` function: It accepts a module id and
  204. // looks for it in the `modules` object we constructed previously. We
  205. // destructure over the two-value array to get our function wrapper and the
  206. // mapping object.
  207. //
  208. // The code of our modules has calls to `require()` with relative file paths
  209. // instead of module ids. Our require function expects module ids. Also, two
  210. // modules might `require()` the same relative path but mean two different
  211. // modules.
  212. //
  213. // To handle that, when a module is required we create a new, dedicated
  214. // `require` function for it to use. It will be specific to that module and
  215. // will know to turn its relative paths into ids by using the module's
  216. // mapping object. The mapping object is exactly that, a mapping between
  217. // relative paths and module ids for that specific module.
  218. //
  219. // Lastly, with CommonJs, when a module is required, it can expose values by
  220. // mutating its `exports` object. The `exports` object, after it has been
  221. // changed by the module's code, is returned from the `require()` function.
  222. const result = `
  223. (function(modules) {
  224. function require(id) {
  225. const [fn, mapping] = modules[id];
  226.  
  227. function localRequire(name) {
  228. return require(mapping[name]);
  229. }
  230.  
  231. const module = { exports : {} };
  232.  
  233. fn(localRequire, module, module.exports);
  234.  
  235. return module.exports;
  236. }
  237.  
  238. require(0);
  239. })({${modules}})
  240. `;
  241.  
  242. // We simply return the result, hurray! :)
  243. return result;
  244. }
  245.  
  246. const graph = createGraph('./example/entry.js');
  247. const result = bundle(graph);
  248.  
  249. console.log(result);
  250.  

来源:https://github.com/ronami/minipack

作者 铁血 汉子 2021年5月28日
2025/05/01/01:56:31pm 2021/5/28/16:25:29
0 1591