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:
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 torequire
-
Replace
export
statements with assignments to theexports
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;