Graduate Program KB

Javascript Bundling - Part 2/2

Accompanying Git Repository

The repository for Brain School 02 workshop can cloned from: codecommit::ap-southeast-2://YOUR-PF-WORKLOAD-DEVELOPER-PROFILE@graduate-programme-resources

Now that we've learned how bundlers work, lets create our own bundler. For this exercise, we'll be creating a Webpack style bundler.

Consider the following modules:

//module-pages.ts
import derp from "./module-b.js";
import flim from "./module-c.js";
console.log(`${flim} and ${derp()}`);
//module-b.js
const derp = () => "derp derp derp";
export default derp;
//module-c.js
import shimSham from "./module-d.js";
const flooFloo = `floo floo ${shimSham}`;
//module-d.js
const shimSham = "shoop shoop shimmy sham sham";
export default shimSham;

The module dependencies are shown below:

module-dependencies

Our goal is to produce an object describing each of our modules, which we can pass to webpackStart, our runtime.

//Module Definitions
const modules = {
  "module-pages.ts": function (exports, require) {
    const derp = require("./module-b.js").default;
    const flim = require("./module-c.js").default;
    console.log(`${flim} and ${derp}`);
  },
  "module-b.js": function (exports, require) {
    const derp = () => "derp derp derp";
    exports.default = derp;
  },
  "module-c.js": function (exports, require) {
    const shimSham = require("./module-d.js").default;
    exports.default = shimSham;
  },
  "module-d.js": function (exports, require) {
    const shimSham = "shoop shoop shimmy sham sham";
    exports.default = shimSham;
  },
};

// This is the "runtime"
webpackStart({
  modules,
  entry: "app.js",
});

To produce the module definitions, we need to formally describe the relationships between all of our modules. The best way to do this is by starting from our entry point, i.e. the top most module in our application - in this case: module-pages.ts. We then visit each of its dependencies. We then repeat the above process for each dependency, untill we have explored the entire dependency tree.

When visiting each module in the tree, we need to:

  • Replace import statements with calls to require

  • Replace export statements with assignments to the exports object:

    const myFunkyFunc = () => console.log("hala hala");
    const coolValue = 76;
    exports = {
      default: myFunkyFunc,
      fooBar: coolValue,
    };
    

The first step in the transformation is to convert the code in each module into an module into an abstract syntax tree using babel.

Click here to use the AST Explorer to visualize the AST for the code in module-pages.ts.

Import Statements

Step 1 - Handle a default import

const assertCodeTransformedCorrectly = (code, transformedCode) => expect(prettier.format(transformer(code), { parser: "babel" })).toEqual(prettier.format(transformedCode, { parser: "babel" }));
describe("transformer.js", () => {
  describe("import statements", () => {
    describe("default imports", () => {
      it("transforms a default import", () => {
        const code = `import a from './module-pages.ts';`;
        const transformedCode = `const {default:a} = require('./module-pages.ts');`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
    });
  });
});
const babel = require("@babel/core");
const transformer = (code) => {
  const { code: transformedCode } = babel.transformSync(code, {
    plugins: [
      function transformerPlugin({ types }) {
        return {
          visitor: {
            ImportDeclaration(path) {
              const properties = path
                .get("specifiers")
                .filter((specifier) => specifier.isImportDefaultSpecifier())
                .map((specifier) => {
                  const key = types.identifier("default");
                  const value = specifier.get("local").node;
                  return types.objectProperty(key, value);
                });
              const modulePath = path.get("source.value").node;
              if (properties.length > 0) {
                path.replaceWith(types.variableDeclaration("const", [types.variableDeclarator(types.objectPattern(properties), types.callExpression(types.identifier("require"), [types.stringLiteral(modulePath)]))]));
              }
            },
          },
        };
      },
    ],
  });
  return transformedCode;
};

module.exports = transformer;

Step 2 - Handle a single non-default import

const transformer = require("./transformer.js");
const prettier = require("prettier");
const assertCodeTransformedCorrectly = (code, transformedCode) => expect(prettier.format(transformer(code), { parser: "babel" })).toEqual(prettier.format(transformedCode, { parser: "babel" }));
describe("transformer.js", () => {
  describe("import statements", () => {
    describe("default imports", () => {
      it("transforms a default import", () => {
        const code = `import a from './module-pages.ts';`;
        const transformedCode = `const {default:a} = require('./module-pages.ts');`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      describe("non-default imports", () => {
        it("transforms a non-default import", () => {
          const code = `import {a} from './module-pages.ts';`;
          const transformedCode = `const {a:a} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
    });
  });
});
const babel = require("@babel/core");
const transformer = (code) => {
  const { code: transformedCode } = babel.transformSync(code, {
    plugins: [
      function transformerPlugin({ types }) {
        return {
          visitor: {
            ImportDeclaration(path) {
              const properties = path.get("specifiers").map((specifier) => {
                const key = types.identifier("default");
                const value = specifier.get("local").node;
                if (specifier.isImportDefaultSpecifier()) {
                  // https://babeljs.io/docs/babel-types#objectproperty
                  return types.objectProperty(key, value);
                } else {
                  const key = specifier.get("imported").node;
                  const value = specifier.get("local").node;
                  return types.objectProperty(key, value, false, false);
                }
              });
              const modulePath = path.get("source.value").node;

              path.replaceWith(types.variableDeclaration("const", [types.variableDeclarator(types.objectPattern(properties), types.callExpression(types.identifier("require"), [types.stringLiteral(modulePath)]))]));
            },
          },
        };
      },
    ],
  });
  return transformedCode;
};

module.exports = transformer;

Step 3 - Handle multiple non-default imports

const transformer = require("./transformer.js");
const prettier = require("prettier");
const assertCodeTransformedCorrectly = (code, transformedCode) => expect(prettier.format(transformer(code), { parser: "babel" })).toEqual(prettier.format(transformedCode, { parser: "babel" }));
describe("transformer.js", () => {
  describe("import statements", () => {
    describe("default imports", () => {
      it("transforms a default import", () => {
        const code = `import a from './module-pages.ts';`;
        const transformedCode = `const {default:a} = require('./module-pages.ts');`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      describe("non-default imports", () => {
        it("transforms a non-default import", () => {
          const code = `import {a} from './module-pages.ts';`;
          const transformedCode = `const {a:a} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
        it("transforms multiple non-default imports", () => {
          const code = `import {a,b} from './module-pages.ts';`;
          const transformedCode = `const {a:a,b:b} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
    });
  });
});

Step 4 - Handle default and non-default imports

const transformer = require("./transformer.js");
const prettier = require("prettier");
const assertCodeTransformedCorrectly = (code, transformedCode) => expect(prettier.format(transformer(code), { parser: "babel" })).toEqual(prettier.format(transformedCode, { parser: "babel" }));
describe("transformer.js", () => {
  describe("import statements", () => {
    describe("default imports", () => {
      it("transforms a default import", () => {
        const code = `import a from './module-pages.ts';`;
        const transformedCode = `const {default:a} = require('./module-pages.ts');`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      describe("non-default imports", () => {
        it("transforms a non-default import", () => {
          const code = `import {a} from './module-pages.ts';`;
          const transformedCode = `const {a:a} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
        it("transforms multiple non-default imports", () => {
          const code = `import {a,b} from './module-pages.ts';`;
          const transformedCode = `const {a:a,b:b} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
      describe("default and non-default imports", () => {
        it("transforms default and non-default imports", () => {
          const code = `import a,{b,c} from './module-pages.ts';`;
          const transformedCode = `const {default:a,b:b,c:c} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
    });
  });
  describe("export statements", () => {});
});

Export Statements

Step 1 - Handle default export

describe("export statements", () => {
  describe("default export", () => {
    it("transforms a default export", () => {
      const code = `export default a;`;
      const transformedCode = `exports.default = a;`;
      assertCodeTransformedCorrectly(code, transformedCode);
    });
  });
});
const babel = require("@babel/core");
const transformer = (code) => {
  const { code: transformedCode } = babel.transformSync(code, {
    plugins: [
      function transformerPlugin({ types }) {
        return {
          visitor: {
            ImportDeclaration(path) {
              const properties = path.get("specifiers").map((specifier) => {
                const key = types.identifier("default");
                const value = specifier.get("local").node;
                if (specifier.isImportDefaultSpecifier()) {
                  // https://babeljs.io/docs/babel-types#objectproperty
                  return types.objectProperty(key, value);
                } else {
                  const key = specifier.get("imported").node;
                  const value = specifier.get("local").node;
                  return types.objectProperty(key, value, false, false);
                }
              });
              const modulePath = path.get("source.value").node;

              path.replaceWith(types.variableDeclaration("const", [types.variableDeclarator(types.objectPattern(properties), types.callExpression(types.identifier("require"), [types.stringLiteral(modulePath)]))]));
            },
            ExportDefaultDeclaration(path) {
              path.replaceWith(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), types.identifier("default")), types.toExpression(path.get("declaration").node))));
            },
          },
        };
      },
    ],
  });
  return transformedCode;
};

module.exports = transformer;

Step 2 - Handle exported class declarations

const transformer = require("./transformer.js");
const prettier = require("prettier");
const assertCodeTransformedCorrectly = (code, transformedCode) => expect(prettier.format(transformer(code), { parser: "babel" })).toEqual(prettier.format(transformedCode, { parser: "babel" }));
describe("transformer.js", () => {
  describe("import statements", () => {
    describe("default imports", () => {
      it("transforms a default import", () => {
        const code = `import a from './module-pages.ts';`;
        const transformedCode = `const {default:a} = require('./module-pages.ts');`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      describe("non-default imports", () => {
        it("transforms a non-default import", () => {
          const code = `import {a} from './module-pages.ts';`;
          const transformedCode = `const {a:a} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
        it("transforms multiple non-default imports", () => {
          const code = `import {a,b} from './module-pages.ts';`;
          const transformedCode = `const {a:a,b:b} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
      describe("default and non-default imports", () => {
        it("transforms default and non-default imports", () => {
          const code = `import a,{b,c} from './module-pages.ts';`;
          const transformedCode = `const {default:a,b:b,c:c} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
    });
  });
  describe("export statements", () => {
    describe("default export", () => {
      it("transforms a default export", () => {
        const code = `export default a;`;
        const transformedCode = `exports.default = a;`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
    });
    describe("non-default exports", () => {
      it("transforms a non-default class declaration", () => {
        const code = `export class FooBaz{}`;
        const transformedCode = `exports.FooBaz = class FooBaz{};`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
    });
  });
});
const babel = require("@babel/core");
const transformer = (code) => {
  const { code: transformedCode } = babel.transformSync(code, {
    plugins: [
      function transformerPlugin({ types }) {
        return {
          visitor: {
            ImportDeclaration(path) {
              const properties = path.get("specifiers").map((specifier) => {
                const key = types.identifier("default");
                const value = specifier.get("local").node;
                if (specifier.isImportDefaultSpecifier()) {
                  // https://babeljs.io/docs/babel-types#objectproperty
                  return types.objectProperty(key, value);
                } else {
                  const key = specifier.get("imported").node;
                  const value = specifier.get("local").node;
                  return types.objectProperty(key, value, false, false);
                }
              });
              const modulePath = path.get("source.value").node;

              path.replaceWith(types.variableDeclaration("const", [types.variableDeclarator(types.objectPattern(properties), types.callExpression(types.identifier("require"), [types.stringLiteral(modulePath)]))]));
            },
            ExportDefaultDeclaration(path) {
              path.replaceWith(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), types.identifier("default")), types.toExpression(path.get("declaration").node))));
            },
            ExportNamedDeclaration(path) {
              if (path.get("declaration").isClassDeclaration()) {
                path.replaceWith(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), path.get("declaration.id").node), types.toExpression(path.get("declaration").node))));
              }
            },
          },
        };
      },
    ],
  });
  return transformedCode;
};

module.exports = transformer;

Step 3 - Handle exported function declarations

const transformer = require("./transformer.js");
const prettier = require("prettier");
const assertCodeTransformedCorrectly = (code, transformedCode) => expect(prettier.format(transformer(code), { parser: "babel" })).toEqual(prettier.format(transformedCode, { parser: "babel" }));
describe("transformer.js", () => {
  describe("import statements", () => {
    describe("default imports", () => {
      it("transforms a default import", () => {
        const code = `import a from './module-pages.ts';`;
        const transformedCode = `const {default:a} = require('./module-pages.ts');`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      describe("non-default imports", () => {
        it("transforms a non-default import", () => {
          const code = `import {a} from './module-pages.ts';`;
          const transformedCode = `const {a:a} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
        it("transforms multiple non-default imports", () => {
          const code = `import {a,b} from './module-pages.ts';`;
          const transformedCode = `const {a:a,b:b} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
      describe("default and non-default imports", () => {
        it("transforms default and non-default imports", () => {
          const code = `import a,{b,c} from './module-pages.ts';`;
          const transformedCode = `const {default:a,b:b,c:c} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
    });
  });
  describe("export statements", () => {
    describe("default export", () => {
      it("transforms a default export", () => {
        const code = `export default a;`;
        const transformedCode = `exports.default = a;`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
    });
    describe("non-default exports", () => {
      it("transforms a non-default class declaration", () => {
        const code = `export class FooBaz{}`;
        const transformedCode = `exports.FooBaz = class FooBaz{};`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      it("transforms a non-default function declaration", () => {
        const code = `export function flimFlam(){};`;
        const transformedCode = `exports.flimFlam = function flimFlam(){};`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
    });
  });
});
const babel = require("@babel/core");
const transformer = (code) => {
  const { code: transformedCode } = babel.transformSync(code, {
    plugins: [
      function transformerPlugin({ types }) {
        return {
          visitor: {
            ImportDeclaration(path) {
              const properties = path.get("specifiers").map((specifier) => {
                const key = types.identifier("default");
                const value = specifier.get("local").node;
                if (specifier.isImportDefaultSpecifier()) {
                  // https://babeljs.io/docs/babel-types#objectproperty
                  return types.objectProperty(key, value);
                } else {
                  const key = specifier.get("imported").node;
                  const value = specifier.get("local").node;
                  return types.objectProperty(key, value, false, false);
                }
              });
              const modulePath = path.get("source.value").node;

              path.replaceWith(types.variableDeclaration("const", [types.variableDeclarator(types.objectPattern(properties), types.callExpression(types.identifier("require"), [types.stringLiteral(modulePath)]))]));
            },
            ExportDefaultDeclaration(path) {
              path.replaceWith(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), types.identifier("default")), types.toExpression(path.get("declaration").node))));
            },
            ExportNamedDeclaration(path) {
              const declaration = path.get("declaration");

              if (declaration.isClassDeclaration() || declaration.isFunctionDeclaration()) {
                path.replaceWith(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), declaration.get("id").node), types.toExpression(declaration.node))));
              }
            },
          },
        };
      },
    ],
  });
  return transformedCode;
};

module.exports = transformer;

Step 4 - Handle exported variable declarations

const transformer = require("./transformer.js");
const prettier = require("prettier");
const assertCodeTransformedCorrectly = (code, transformedCode) => expect(prettier.format(transformer(code), { parser: "babel" })).toEqual(prettier.format(transformedCode, { parser: "babel" }));
describe("transformer.js", () => {
  describe("import statements", () => {
    describe("default imports", () => {
      it("transforms a default import", () => {
        const code = `import a from './module-pages.ts';`;
        const transformedCode = `const {default:a} = require('./module-pages.ts');`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      describe("non-default imports", () => {
        it("transforms a non-default import", () => {
          const code = `import {a} from './module-pages.ts';`;
          const transformedCode = `const {a:a} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
        it("transforms multiple non-default imports", () => {
          const code = `import {a,b} from './module-pages.ts';`;
          const transformedCode = `const {a:a,b:b} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
      describe("default and non-default imports", () => {
        it("transforms default and non-default imports", () => {
          const code = `import a,{b,c} from './module-pages.ts';`;
          const transformedCode = `const {default:a,b:b,c:c} = require('./module-pages.ts');`;
          assertCodeTransformedCorrectly(code, transformedCode);
        });
      });
    });
  });
  describe("export statements", () => {
    describe("default export", () => {
      it("transforms a default export", () => {
        const code = `export default a;`;
        const transformedCode = `exports.default = a;`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
    });
    describe("non-default exports", () => {
      it("transforms a non-default class declaration", () => {
        const code = `export class FooBaz{}`;
        const transformedCode = `exports.FooBaz = class FooBaz{};`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      it("transforms a non-default function declaration", () => {
        const code = `export function flimFlam(){};`;
        const transformedCode = `exports.flimFlam = function flimFlam(){};`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
      it("transforms non-default variable declarations", () => {
        const code = `export const zoomZoomm = 3, flurpDerp = "simsim";`;
        const transformedCode = `exports.zoomZoom = 3; exports.flurpDerp = "simsim";`;
        assertCodeTransformedCorrectly(code, transformedCode);
      });
    });
  });
});
const babel = require("@babel/core");
const transformer = (code) => {
  const { code: transformedCode } = babel.transformSync(code, {
    plugins: [
      function transformerPlugin({ types }) {
        return {
          visitor: {
            ImportDeclaration(path) {
              const properties = path.get("specifiers").map((specifier) => {
                const key = types.identifier("default");
                const value = specifier.get("local").node;
                if (specifier.isImportDefaultSpecifier()) {
                  // https://babeljs.io/docs/babel-types#objectproperty
                  return types.objectProperty(key, value);
                } else {
                  const key = specifier.get("imported").node;
                  const value = specifier.get("local").node;
                  return types.objectProperty(key, value, false, false);
                }
              });
              const modulePath = path.get("source.value").node;

              path.replaceWith(types.variableDeclaration("const", [types.variableDeclarator(types.objectPattern(properties), types.callExpression(types.identifier("require"), [types.stringLiteral(modulePath)]))]));
            },
            ExportDefaultDeclaration(path) {
              path.replaceWith(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), types.identifier("default")), types.toExpression(path.get("declaration").node))));
            },
            ExportNamedDeclaration(path) {
              const declaration = path.get("declaration");

              if (declaration.isClassDeclaration() || declaration.isFunctionDeclaration()) {
                path.replaceWith(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), declaration.get("id").node), types.toExpression(declaration.node))));
              } else if (declaration.isVariableDeclaration()) {
                const variableDeclarations = [];
                path.get("declaration.declarations").forEach((variableDeclaration) =>
                  variableDeclarations.push({
                    name: variableDeclaration.get("id").node,
                    value: variableDeclaration.get("init").node,
                  })
                );
                path.replaceWithMultiple(variableDeclarations.map((variableDeclaration) => types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.identifier("exports"), variableDeclaration.name), variableDeclaration.value))));
              }
            },
          },
        };
      },
    ],
  });
  return transformedCode;
};

module.exports = transformer;

Path resolution

Resolving relative path imports

const filePathResolver = require("./create-file-path-resolver.js");
describe("create-file-path-resolver", () => {
  describe("given the absolute path of a file", () => {
    describe("and the relative path to a requested file", () => {
      it("returns the absolute path of the requested file", () => {
        const requestingFilePath = "/path/to/requester.js";
        const relativePathToRequestedFile = "../some-other-file.js";
        const absolutePathToRequestedFile = "/path/some-other-file.js";
        expect(filePathResolver(requestingFilePath)(relativePathToRequestedFile)).toEqual(absolutePathToRequestedFile);
      });
    });
  });
});
const path = require("path");
const createFilePathResolver = (requestingFilePath) => (requestedFilePath) => {
  return path.join(path.dirname(requestingFilePath), requestedFilePath);
};
module.exports = createFilePathResolver;

Module dependencies

Step 1 - Module with no dependencies

const createModuleDependencyGraph = require("./create-module-dependency-graph.js");
const path = require("path");
describe("create-module-dependency-graph", () => {
  describe("given a single module: A", () => {
    it("returns a graph with a single node", () => {
      const relativePathToModuleB = "./test-modules/folder-a/folder-b/module-b.js";
      const absolutePathToModuleB = path.join(__dirname, "./test-modules/folder-a/folder-b/module-b.js");
      expect(createModuleDependencyGraph(__dirname, relativePathToModuleB)).toEqual(
        expect.objectContaining({
          filePath: absolutePathToModuleB,
          dependencies: [],
        })
      );
    });
  });
});
const path = require("path");
const createModuleDependencyGraph = (absolutePathOfParentModule, relativeFilePath) => ({
  filePath: path.join(absolutePathOfParentModule, relativeFilePath),
  dependencies: [],
});
module.exports = createModuleDependencyGraph;

Step 2 - Two modules with one dependent on the other

const createModuleDependencyGraph = require("./create-module-dependency-graph.js");
const path = require("path");
describe("create-module-dependency-graph", () => {
  describe("given a single module: A", () => {
    it("returns a graph with a single node", () => {
      const relativePathToModuleB = "./test-modules/folder-a/folder-b/module-b.js";
      const absolutePathToModuleB = path.join(__dirname, "./test-modules/folder-a/folder-b/module-b.js");
      expect(createModuleDependencyGraph(__dirname, relativePathToModuleB)).toEqual(
        expect.objectContaining({
          filePath: absolutePathToModuleB,
          dependencies: [],
        })
      );
    });
  });
  describe("given 2 modules: B<--depends-on--A", () => {
    it("returns a graph with two nodes", () => {
      const relativePathToModuleA = "./test-modules/folder-a/module-pages.ts";
      const absolutePathToModuleA = path.join(__dirname, "./test-modules/folder-a/module-pages.ts");
      const absolutePathToModuleB = path.join(__dirname, "./test-modules/folder-a", "./folder-b/module-b.js");
      expect(createModuleDependencyGraph(__dirname, relativePathToModuleA)).toEqual(
        expect.objectContaining({
          filePath: absolutePathToModuleA,
          dependencies: expect.arrayContaining([
            expect.objectContaining({
              filePath: absolutePathToModuleB,
              dependencies: [],
            }),
          ]),
        })
      );
    });
  });
});
const path = require("path");
const babel = require("@babel/core");
const fs = require("fs");
const createTransformer = require("./transformer.js").createTransformer;
const createFilePathResolver = require("./create-file-path-resolver");
const createModuleDependencyGraph = (absolutePathOfParentModule, relativeFilePath) => {
  const absoluteFilePath = path.join(absolutePathOfParentModule, relativeFilePath);
  const _content = fs.readFileSync(absoluteFilePath, "utf-8");
  const ast = babel.parseSync(_content);
  const dependencies = ast.program.body
    .filter((node) => node.type === "ImportDeclaration")
    .map((node) => node.source.value)
    .map((relativeFilePathOfDependency) => createModuleDependencyGraph(path.dirname(absoluteFilePath), relativeFilePathOfDependency));

  return {
    filePath: absoluteFilePath,
    dependencies,
    get content() {
      delete this.content;
      const transformer = createTransformer(createFilePathResolver(this.filePath));
      this.content = transformer(_content, this.filePath);
      return this.content;
    },
  };
};
module.exports = createModuleDependencyGraph;

Step 3 - Three inter-dependent modules

const path = require("path");
const babel = require("@babel/core");
const fs = require("fs");
const createTransformer = require("./transformer.js").createTransformer;
const createFilePathResolver = require("./create-file-path-resolver");
const createModuleDependencyGraph = (absolutePathOfParentModule, relativeFilePath) => {
  const absoluteFilePath = path.join(absolutePathOfParentModule, relativeFilePath);
  const _content = fs.readFileSync(absoluteFilePath, "utf-8");
  const ast = babel.parseSync(_content);
  const dependencies = ast.program.body
    .filter((node) => node.type === "ImportDeclaration")
    .map((node) => node.source.value)
    .map((relativeFilePathOfDependency) => createModuleDependencyGraph(path.dirname(absoluteFilePath), relativeFilePathOfDependency));

  return {
    filePath: absoluteFilePath,
    dependencies,
  };
};
module.exports = createModuleDependencyGraph;
const path = require("path");
const babel = require("@babel/core");
const fs = require("fs");
const createTransformer = require("./transformer.js").createTransformer;
const createFilePathResolver = require("./create-file-path-resolver");
const createModuleDependencyGraph = (absolutePathOfParentModule, relativeFilePath) => {
  const absoluteFilePath = path.join(absolutePathOfParentModule, relativeFilePath);
  const _content = fs.readFileSync(absoluteFilePath, "utf-8");
  const ast = babel.parseSync(_content);
  const dependencies = ast.program.body
    .filter((node) => node.type === "ImportDeclaration")
    .map((node) => node.source.value)
    .map((relativeFilePathOfDependency) => createModuleDependencyGraph(path.dirname(absoluteFilePath), relativeFilePathOfDependency));

  return {
    filePath: absoluteFilePath,
    dependencies,
  };
};
module.exports = createModuleDependencyGraph;

Module Map

Step 1 - Module map for a module dependency graph containing 1 module

const path = require("path");
const createModuleDependencyGraph = require("./create-module-dependency-graph.js");
const createModuleMap = require("./create-module-map.js");
describe("create-module-map", () => {
  describe("given a module dependency graph with a single module", () => {
    it("returns a module map with single entry", () => {
      const moduleDependencyGraph = createModuleDependencyGraph(__dirname, "./test-modules/folder-a/folder-b/module-b.js");

      expect(createModuleMap(moduleDependencyGraph)).toMatchInlineSnapshot(`
        "{ '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
        exports.default = b; } }"
      `);
    });
  });
});
const collectModules = (module, modules = []) => {
  modules.push(module);
  module.dependencies.forEach((dependency) => {
    collectModules(dependency, modules);
  });
  return modules;
};

const createModuleMap = (moduleDepedencyGraph) => {
  const modules = collectModules(moduleDepedencyGraph);
  const moduleEntries = modules.map((module) => `'${module.filePath}': function (exports,require){ ${module.content} }`).join(",");
  return `{ ${moduleEntries} }`;
};
module.exports = createModuleMap;

Step 2 - Module map for a module dependency graph containing multiple dependent modules

const path = require("path");
const createModuleDependencyGraph = require("./create-module-dependency-graph.js");
const createModuleMap = require("./create-module-map.js");
describe("create-module-map", () => {
  describe("given a module dependency graph with a single module", () => {
    it("returns a module map with single entry", () => {
      const moduleDependencyGraph = createModuleDependencyGraph(__dirname, "./test-modules/folder-a/folder-b/module-b.js");

      expect(createModuleMap(moduleDependencyGraph)).toMatchInlineSnapshot(`
        "{ '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
        exports.default = b; } }"
      `);
    });
  });
  describe("given a module dependency graph with a multiple, dependent modules", () => {
    it("returns a module map with an entry for each module", () => {
      const moduleDependencyGraph = createModuleDependencyGraph(__dirname, "./test-modules/module-c.js");

      expect(createModuleMap(moduleDependencyGraph)).toMatchInlineSnapshot(`
        "{ '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/module-c.js': function (exports,require){ const {
          default: a
        } = require("/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/module-pages.ts");
        exports.z = "luluLimen";
        console.log(\`So yeah, \${a}\`); },'/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/module-pages.ts': function (exports,require){ const {
          default: b
        } = require("/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js");
        const a = "hey " + b;
        exports.default = a; },'/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
        exports.default = b; } }"
      `);
    });
  });
});
const collectModules = (module, modules = []) => {
  modules.push(module);
  module.dependencies.forEach((dependency) => {
    collectModules(dependency, modules);
  });
  return modules;
};

const createModuleMap = (moduleDepedencyGraph) => {
  const modules = collectModules(moduleDepedencyGraph);
  const moduleEntries = modules.map((module) => `'${module.filePath}': function (exports,require){ ${module.content} }`).join(",");
  return `{ ${moduleEntries} }`;
};
module.exports = createModuleMap;

Create Runtime

Step 1 - Runtime for a module map of size 1

const createRuntime = require("./create-runtime.js");
describe("create-runtime", () => {
  describe("given an entry point,", () => {
    describe("and a module map containing a single module", () => {
      it("returns a runtime function", () => {
        const moduleMap = `{ '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
        exports.default = b; } }`;
        const entryPoint = "/code/brain-school/02-js-bundling-part-2-of-2/old/tdd/test-modules/folder-a/folder-b/module-b.js";
        expect(createRuntime(moduleMap, entryPoint)).toMatchInlineSnapshot(`
          "const modules = { '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
          exports.default = b; } };
          const entry = "/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js";
          function webpackStart({modules,entry}){
          const moduleCache = {};
          //Webpack require function
          const require = moduleName => {
          //If the module is cached, return the cached version
          if(moduleCache[moduleName]){
          return moduleCache[moduleName];
          }
          const exports = {};
          //We need to avoid the cyclical dependencies
          //when invoking require()
          moduleCache[moduleName] = exports;
          //require() the module 
          modules[moduleName](exports,require);
          return moduleCache[moduleName];
          };
          //Execute the program
          require(entry);
          }
          webpackStart({modules,entry});
          "
        `);
      });
    });
  });
});
const createRuntime = (moduleMap, entryPoint) => {
  return trim(`
          const modules = ${moduleMap};
          const entry = "${entryPoint}";
          function webpackStart({modules,entry}){
            
            const moduleCache = {};
            
            //Webpack require function
            const require = moduleName => {
              //If the module is cached, return the cached version
              if(moduleCache[moduleName]){
                return moduleCache[moduleName];
              }
              const exports = {};
              //We need to avoid the cyclical dependencies
              //when invoking require()
              moduleCache[moduleName] = exports;
              
              //require() the module 
              modules[moduleName](exports,require);
              return moduleCache[moduleName];
      
            };
      
            //Execute the program
            require(entry);
            
          }
          webpackStart({modules,entry});
        `);
};

const trim = (code) => code.replace(/^\s+(.*)$/gm, "$1");

module.exports = createRuntime;

Step 1 - Runtime for a module map of size 2

const createRuntime = require("./create-runtime.js");
describe("create-runtime", () => {
  describe("given an entry point,", () => {
    describe("and a module map containing a single module", () => {
      it("returns a runtime function", () => {
        const moduleMap = `{ '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
        exports.default = b; } }`;
        const entryPoint = "/code/brain-school/02-js-bundling-part-2-of-2/old/tdd/test-modules/folder-a/folder-b/module-b.js";
        expect(createRuntime(moduleMap, entryPoint)).toMatchInlineSnapshot(`
          "const modules = { '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
          exports.default = b; } };
          const entry = "/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js";
          function webpackStart({modules,entry}){
          const moduleCache = {};
          //Webpack require function
          const require = moduleName => {
          //If the module is cached, return the cached version
          if(moduleCache[moduleName]){
          return moduleCache[moduleName];
          }
          const exports = {};
          //We need to avoid the cyclical dependencies
          //when invoking require()
          moduleCache[moduleName] = exports;
          //require() the module 
          modules[moduleName](exports,require);
          return moduleCache[moduleName];
          };
          //Execute the program
          require(entry);
          }
          webpackStart({modules,entry});
          "
        `);
      });
    });
    describe("and a module map containing a single module", () => {
      it("returns a runtime function", () => {
        const moduleMap = `{ '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
          exports.default = b; } }`;
        const entryPoint = "/code/brain-school/02-js-bundling-part-2-of-2/old/tdd/test-modules/folder-a/folder-b/module-b.js";
        expect(createRuntime(moduleMap, entryPoint)).toMatchInlineSnapshot(`
            "const modules = { '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
            exports.default = b; } };
            const entry = "/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js";
            function webpackStart({modules,entry}){
            const moduleCache = {};
            //Webpack require function
            const require = moduleName => {
            //If the module is cached, return the cached version
            if(moduleCache[moduleName]){
            return moduleCache[moduleName];
            }
            const exports = {};
            //We need to avoid the cyclical dependencies
            //when invoking require()
            moduleCache[moduleName] = exports;
            //require() the module 
            modules[moduleName](exports,require);
            return moduleCache[moduleName];
            };
            //Execute the program
            require(entry);
            }
            webpackStart({modules,entry});
            "
          `);
      });
    });
  });
});
const createRuntime = (moduleMap, entryPoint) => {
  return trim(`
          const modules = ${moduleMap};
          const entry = "${entryPoint}";
          function webpackStart({modules,entry}){
            
            const moduleCache = {};
            
            //Webpack require function
            const require = moduleName => {
              //If the module is cached, return the cached version
              if(moduleCache[moduleName]){
                return moduleCache[moduleName];
              }
              const exports = {};
              //We need to avoid the cyclical dependencies
              //when invoking require()
              moduleCache[moduleName] = exports;
              
              //require() the module 
              modules[moduleName](exports,require);
              return moduleCache[moduleName];
      
            };
      
            //Execute the program
            require(entry);
            
          }
          webpackStart({modules,entry});
        `);
};

const trim = (code) => code.replace(/^\s+(.*)$/gm, "$1");

module.exports = createRuntime;

Bundle

const fs = require("fs");
const path = require("path");
const bundle = require("./bundle");

describe("bundle", () => {
  describe("given an entry point and an output folder", () => {
    const outputDirPath = path.join(__dirname, ".tmp");
    beforeAll(() => {
      if (!fs.existsSync(outputDirPath)) {
        fs.mkdirSync(outputDirPath);
      }
    });

    it("writes a bundle into the output folder", () => {
      bundle({
        entryFile: "./test-modules/module-c.js",
        entryFilePath: __dirname,
        outputFolder: outputDirPath,
      });
      const bundledCode = fs.readFileSync(path.join(outputDirPath, "bundle.js"), {
        encoding: "utf-8",
      });
      expect(bundledCode).toMatchInlineSnapshot(`
        "const modules = { '/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/module-c.js': function (exports,require){ const {
        default: a
        } = require("/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/module-pages.ts");
        exports.z = "luluLimen";
        console.log(\`So yeah, \${a}\`); },'/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/module-pages.ts': function (exports,require){ const {
        default: b
        } = require("/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js");
        const a = "hey " + b;
        exports.default = a; },'/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/folder-a/folder-b/module-b.js': function (exports,require){ const b = "bebe";
        exports.default = b; } };
        const entry = "/code/brain-school/02-js-bundling-part-2-of-2/src/tdd/test-modules/module-c.js";
        function webpackStart({modules,entry}){
        const moduleCache = {};
        //Webpack require function
        const require = moduleName => {
        //If the module is cached, return the cached version
        if(moduleCache[moduleName]){
        return moduleCache[moduleName];
        }
        const exports = {};
        //We need to avoid the cyclical dependencies
        //when invoking require()
        moduleCache[moduleName] = exports;
        //require() the module 
        modules[moduleName](exports,require);
        return moduleCache[moduleName];
        };
        //Execute the program
        require(entry);
        }
        webpackStart({modules,entry});
        "
      `);
    });
  });
});
const createModuleDependencyGraph = require("./create-module-dependency-graph");
const createModuleMap = require("./create-module-map");
const createRuntime = require("./create-runtime");
const fs = require("fs");
const path = require("path");
const bundle = ({ entryFile, entryFilePath, outputFolder }) => {
  const moduleDependencyGraph = createModuleDependencyGraph(entryFilePath, entryFile);
  const moduleMap = createModuleMap(moduleDependencyGraph);
  const runtime = createRuntime(moduleMap, path.join(entryFilePath, entryFile));
  fs.writeFileSync(path.join(outputFolder, "bundle.js"), runtime, "utf-8");
};

module.exports = bundle;

References: