WebPack 3. From a config file to the deeps

Ilya Zykin
4 min readMay 30, 2018

Everything starts from execution of the bin file of WebPack. First of all we should pass a config file this executable file.

./node_modules/webpack/bin/webpack.js --config webpack.config.js

What does executable file have inside? Let’s check.

webpack/bin/webpack.js

At the start of the file we see the following

var yargs = require("yargs")

This lib helps to parse parameters.

Some parameters are described in the file config-yargs.js

require("./config-yargs")(yargs);

That what we have here:

module.exports = function(yargs) {
yargs
.help("help")
.alias("help", "h")
.version()
.alias("version", "v")
.options({
"config": {
type: "string",
describe: "Path to the config file",
group: CONFIG_GROUP,
defaultDescription: "webpack.config.js or webpackfile.js",
requiresArg: true
},

...
"p": {
type: "boolean",
describe: "shortcut for --optimize-minimize --define process.env.NODE_ENV=\"production\"",
group: BASIC_GROUP
}
}).strict();

and some options are described in bin/webpack.js

yargs.options({
"json": {
type: "boolean",
alias: "j",
describe: "Prints the result as JSON."
},
... "verbose": {
type: "boolean",
group: DISPLAY_GROUP,
describe: "Show more details"
}
});

After that we go to parsing

yargs.parse(process.argv.slice(2), (err, argv, output) => {  ...  var options = require("./convert-argv")(yargs, argv);  function processOptions(options) { ... }  processOptions(options);
});

Inside processOptions function we have we have including and a call of WebPack compiler.

function processOptions(options) {
var webpack = require("../lib/webpack.js");

try {
compiler = webpack(options); } catch(err) { ... }
}

It’s time to visit webpack/lib/webpack.js file

const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
const NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin");const WebpackOptionsApply = require("./WebpackOptionsApply");const WebpackOptionsDefaulter = require("./WebpackOptionsDefaulter");const validateSchema = require("./validateSchema");const WebpackOptionsValidationError = require("./WebpackOptionsValidationError");const webpackOptionsSchema = require("../schemas/webpackOptionsSchema.json");

There we see some require functions. They include Compiler and MultiCompiler and some other modules. But let’s go to the webpack function.

At the start this function validates the options schema.

function webpack(options, callback) {
const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
if(webpackOptionsValidationErrors.length) { ... } ...}

If everything is fine we do the following

function webpack(options, callback) {
...
new WebpackOptionsDefaulter().process(options); compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler); if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
compiler.options = new WebpackOptionsApply().process(options, compiler);
... if(callback) { compiler.run(callback); }
}

As far as can see after we created a new compiler we set some plugins, some options and run the compiler.

Let’s take a look at Compiler (webpack/lib/Compiler.js)

const Compilation = require("./Compilation");class Compiler extends Tapable {
constructor() { ... }
watch() { ... }
run() { ... }
newCompilationParams() { ... }
compile() { ... }
runAsChild() { ... }
purgeInputFileSystem() { ... }
emitAssets() { ... }
emitRecords() { ... }
readRecords() { ... }

createChildCompiler() { ... }
isChild() { ... }

createCompilation() { ... }
newCompilation() { ... }
createNormalModuleFactory() { ... }
createContextModuleFactory() { ... }
}

From the example above we see that we should learn run method.

run(callback) {
const startTime = Date.now();
...

const onCompiled = (err, compilation) => { ... }
this.applyPluginsAsync("before-run", this, err => {
...

this.applyPluginsAsync("run", this, err => {
...

this.readRecords(err => {
this.compile(callback = onCompiled);
});
});
});
}

Go deeper to the compile method.

compile(callback) {
const params = this.newCompilationParams();
this.applyPluginsAsync("before-compile", params, err => {
...
this.applyPlugins("compile", params);
const compilation = this.newCompilation(params);
this.applyPluginsParallel("make", compilation, err => {
...
compilation.finish();
compilation.seal(err => {
...
this.applyPluginsAsync("after-compile",compilation, err => {
...
return callback(null, compilation);
})
})
})
})
}

newCompilation here is:

newCompilation(params) {
const compilation = this.createCompilation();
... this.applyPlugins("this-compilation", compilation, params);
this.applyPlugins("compilation", compilation, params);

return compilation;
}

And createCompilation is just:

const Compilation = require("./Compilation")...createCompilation() {
return new Compilation(this);
}

The basic stack of call that we have here looks so:

this.applyPlugins("compile", params)
const compilation = this.newCompilation(params)
compilation.finish()
compilation.seal()

It’s time to check what we have in Compilation.js and what finish and seal methods do:

finish() {
const modules = this.modules;
this.applyPlugins1("finish-modules", modules);
for(let index = 0; index < modules.length; index++) {
const module = modules[index];
this.reportDependencyErrorsAndWarnings(module, [module]);
}
}

and

seal(callback) {
this.applyPlugins0("seal");
...
this.applyPlugins2("revive-modules", this.modules, this.records);
this.applyPlugins1("optimize-module-order", this.modules);
... this.applyPlugins1("before-module-ids", this.modules);
this.applyPlugins1("module-ids", this.modules);
this.applyModuleIds();
this.applyPlugins1("optimize-module-ids", this.modules);
this.applyPlugins1("after-optimize-module-ids", this.modules);
...
}

We can see that seal method is mostly a set of plugins’ calls.

It’s interesting that we can see applyPlugins0, applyPlugins1, applyPlugins2. What is the difference? The difference is just a number of arguments that we pass to the method. See more here.

When we reach the end of seal method we perform some final callbacks

this.applyPluginsAsync("additional-assets", err => {  this.applyPluginsAsync("optimize-chunk-assets", ... , err => {
this.applyPlugins1("after-optimize-chunk-assets", this.chunks)

this.applyPluginsAsync("optimize-assets", this.assets, err => {
...
this.applyPlugins1("after-optimize-assets", this.assets)

...
return this.applyPluginsAsync("after-seal", callback)
})
})
})

--

--

Ilya Zykin

IT coach. React and Rails enthusiast. Passionate programmer, caring husband and pancake baker on Sundays. School teacher of computer studies in the past.