SoFunction
Updated on 2025-04-10

Detailed explanation of the compile process of webpack source code-rules parameter processing skills (2)

The previous article introduced it to youDetailed explanation of the compile process-rules parameter processing skills of webpack source code (1),    Detailed explanation of the compile process of webpack source code-entry function run

You can click to view.

The first step is to process the rule as a string, and directly return a wrapper class. It is easy to read the comments.

test

Then process test, include, and exclude, as follows:

if ( ||  || ) {
 // Mark usage parameters checkResourceSource("test + include + exclude");
 // No, it's undefined condition = {
 test: ,
 include: ,
 exclude: 
 };
 // Handle general parameters try {
  = (condition);
 } catch (error) {
 throw new Error((condition, error));
 }
}

CheckResourceSource directly see the source code:

let resourceSource;
// ...
function checkResourceSource(newSource) {
 // Skip directly to the back and assign value for the first time if (resourceSource && resourceSource !== newSource)
 throw new Error((rule, new Error("Rule can only have one resource source (provided " + newSource + " and " + resourceSource + ")")));
 resourceSource = newSource;
}

This is used to detect the uniqueness of the configuration source, and you will see the function later. The same function also includes the checkUseSource method.

Then wrap three parameters into an object and pass them into the normalizeCondition method, which wraps the regular parameters function:

class RuleSet {
 constructor(rules) { /**/ };
 static normalizeCondition(condition) {
 // False value error if (!condition) throw new Error("Expected condition but got falsy value");
 // Detect whether a given string starts with this if (typeof condition === "string") { return str => (condition) === 0; }
 // The function returns directly if (typeof condition === "function") { return condition; }
 // Regular expression returns a regular test function if (condition instanceof RegExp) { return (condition); }
 // Array map recursive processing has a satisfying return true if ((condition)) {
  const items = (c => (c));
  return orMatcher(items);
 }
 if (typeof condition !== "object") throw Error("Unexcepted " + typeof condition + " when condition was expected (" + condition + ")");
 const matchers = [];
 // The object will function wrap each value and pop it into matchers (condition).forEach(key => {
  const value = condition[key];
  switch (key) {
  case "or":
  case "include":
  case "test":
   if (value)
   ((value));
   break;
  case "and":
   if (value) {
   const items = (c => (c));
   (andMatcher(items));
   }
   break;
  case "not":
  case "exclude":
   if (value) {
   const matcher = (value);
   (notMatcher(matcher));
   }
   break;
  default:
   throw new Error("Unexcepted property " + key + " in condition");
  }
 });
 if ( === 0)
  throw new Error("Excepted condition but got " + condition);
 if ( === 1)
  return matchers[0];
 return andMatcher(matchers);
 }
}

Here we use js rules as a case, and see the return of this method:

class RuleSet {
 constructor(rules) { /**/ };
 /*
 Example:
 {
  test: /\.js$/,
  loader: 'babel-loader',
  include: [resolve('src'), resolve('test')]
 }
 */
 /*
 condition:
 {
  test: /\.js$/,
  include: ['d:\\workspace\\src', 'd:\\workspace\\test'],
  exclude: undefined
 }
 */
 static normalizeCondition(condition) {
 // include returns a function similar to [(str) => ('d:\\workspace\\src') === 0,...] if (typeof condition === "string") { return str => (condition) === 0; }
 // The test parameter returns the /\.js$/.test function if (condition instanceof RegExp) { return (condition); }
 // include as array if ((condition)) {
  const items = (c => (c));
  return orMatcher(items);
 }
 const matchers = [];
 // parse out ['test','include','exclude'] (condition).forEach(key => {
  const value = condition[key];
  switch (key) {
  // This value is an array  case "include":
  case "test":
   if (value)
   ((value));
   break;
  // undefined skip  case "exclude":
   if (value) { /**/ }
   break;
  default:
   throw new Error("Unexcepted property " + key + " in condition");
  }
 });
 return andMatcher(matchers);
 }
}

Here we continue to look at the processing of orMatcher and Matcher functions:

function orMatcher(items) {
 // Return true when a function meets the condition return function(str) {
 for (let i = 0; i < ; i++) {
  if (items[i](str))
  return true;
 }
 return false;
 };
}

function andMatcher(items) {
 // Return false when a condition is not satisfied return function(str) {
 for (let i = 0; i < ; i++) {
  if (!items[i](str))
  return false;
 }
 return true;
 };
}

The function can also be understood from the literal meaning. For example, the include here is wrapped into an orMatcher function. The passed string will return true no matter whether it starts with any element in the array, and the same is true for the andMatcher and the notMatcher that does not appear.

resource

The following is the processing of resource parameters, the source code is as follows:

if () {
 // If the test || include || exclude parameter is detected earlier, an error will be reported here checkResourceSource("resource");
 try {
  = ();
 } catch (error) {
 throw new Error((, error));
 }
}

It can be seen that this parameter is mutually exclusive to the previous one, and should be an old version of the API. The following two methods are implemented the same:

/*
 Method 1:
  rules:[
  {
   test: /\.js$/,
   loader: 'babel-loader',
   include: [resolve('src'), resolve('test')]
  }
  ]
 */
/*
 Method 2:
  rules:[
  {
   resource:{
   test: /\.js$/,
   include: [resolve('src'), resolve('test')]
   exclude: undefined
   }
  }
  ]
 */

The next resourceQuery, compiler, issuer will be skipped first. There is no scaffolding, so I don’t know how to write it.

loader

Here is another main configuration loader(s). Here, loader and loaders function the same as loaders. What's the difference between the headache at the beginning:

const loader =  || ;
// Single loader situationif (typeof loader === "string" && ! && !) {
 checkUseSource("loader");
  = (("!"), ident);
}
// loader appears with options or queryelse if (typeof loader === "string" && ( || )) {
 checkUseSource("loader + options/query");
  = ({
 loader: loader,
 options: ,
 query: 
 }, ident);
}
// Errors occurred at the same time as options and queryelse if (loader && ( || )) {
 throw new Error((rule, new Error("options/query cannot be used with loaders (use options for each array item)")));
}
/*
  When dealing with this stupid usage:
  {
  test: /\.css$/,
  loader: [{ loader: 'less-loader' }, { loader: 'css-loader' }]
  }
 */
else if (loader) {
 checkUseSource("loaders");
  = (loader, ident);
}
// Errors occur separately or queryelse if ( || ) {
 throw new Error((rule, new Error("options/query provided without loader (use loader + options)")));
}

The babel-loader mentioned earlier is the first single loader configuration. Here, the vue-loader nested css configuration is used as an example.

First, the normalizeUse method is as follows:

static normalizeUse(use, ident) {
 // Single loader string if ((use)) {
 return use
  // If it is a single loader, it will return [[loader1...],[loader2...]]  .map((item, idx) => (item, `${ident}-${idx}`))
  // After flattening, it becomes [loader1, loader2]  .reduce((arr, items) => (items), []);
 }
 // Object or string return [(use, ident)];
};

First explain the pattern with options or query. Here we will wrap the parameter object and pass it into the normalizeUse method:

loader && (options || query)

// indet => 'ref-'
static normalizeUseItem(item, ident) {
 if (typeof item === "function")
 return item;
 if (typeof item === "string") {
 return (item);
 }
 const newItem = {};
 if ( && ) throw new Error("Provided options and query in use");
 if (!) throw new Error("No loader specified");
  =  || ;
 // Prevent options:null if (typeof  === "object" && ) {
 // This is just to handle the identifier parameter if ()
   = ;
 else
   = ident;
 }
 // Remove the loader parameters const keys = (item).filter(function(key) {
 return ["options", "query"].indexOf(key) < 0;
 });
 (function(key) {
 newItem[key] = item[key];
 });
 /*
  newItem =
  {
  loader:'original string',
  ident:'ref-', (or custom)
  options:{...}
  }
  */
 return newItem;
}

Compared to the processing of test, here is much simpler, briefly described as follows:

1. Try to take out the ident parameter in options(query) and assign it to the ident of newItem. If not, assign it as the default ref-

2. Take out the keys that are not options or query in the object and assign them to newItem. There are only three keys passed in here, and the rest is the loader. This filter is teasing me???

3. Return newItem object

In short, this code is actually very simple if you don’t know why you should prevent any unexpected situations.

Single loader

The second case is a single string, which will cut and call the map method for processing '!', and then call the reduce method to flatten it into an array. Let's look at the normalizeUseItemString method directly:

/*
Example:
{
 test: /\.css$/,
 loader: 'css-loader?{opt:1}!style-loader'
}

return:

[{loader:'css-loader',options:{opt:1}},
{loader:'style-loader'}]
*/
static normalizeUseItemString(useItemString) {
 // Get the loader parameters according to '?' const idx = ("?");
 if (idx >= 0) {
 return {
  loader: (0, idx),
  // The following returns as options  options: (idx + 1)
 };
 }
 return {
 loader: useItemString
 };
}

This is the way to process the serial call loader, and the code is very simple.

Tips

There is one thing to note here: once the loader uses the serial call method, do not pass options or query parameters, otherwise the loader will not be cut and parsed! ! !

These two methods are just different usage methods, and the final result is the same.

use

The following is the analysis of the use parameter. It is estimated that because this is the old version of the API, the format is strict and the processing is relatively casual:

if () {
 checkUseSource("use");
  = (, ident);
}

If you do not use loader(s), you can use use instead, but it needs to be written in the format. For example, the loader in the above serial abbreviation needs to be written in the use as follows:

/*
 {
 test:/\.css$/,
 user:['css-loader','style-loader]
 }
*/

For the corresponding options or query, you need to write it like this:

/*
 {
 test:/\.css$/,
 user:{
  loader:'css-loader',
  options:'1'
 }
 }
*/

Because it is basically no longer needed, I will take a brief look here.

rules、oneOf
if ()
  = (, refs, `${ident}-rules`);
if ()
  = (, refs, `${ident}-oneOf`);

These two are used less and there is nothing difficult to understand.

The next step is to filter out the parameters that are not processed and add them to newRuls:

const keys = (rule).filter((key) => {
 return ["resource", "resourceQuery", "compiler", "test", "include", "exclude", "issuer", "loader", "options", "query", "loaders", "use", "rules", "oneOf"].indexOf(key) < 0;
});
((key) => {
 newRule[key] = rule[key];
});

Basically, test, loader, and options are used. I don’t know any additional parameters for the time being.

ident

// Prevent the rules:[] situationif (()) {
 ((item) =&gt; {
 // ident comes from the ident parameter of options/query if () {
  refs[] = ;
 }
 });
}

In the end, the pure object refs passed in finally used in this place.

If an identifier parameter is passed in options, the object will be filled with the key as the identifier value and the value is the corresponding options.

At this point, all rules for rules have been parsed, which is really simple to configure and complicated to handle.

Summarize

The above is the detailed description of the compile process-rules parameter processing techniques (2) of the webpack source code, I hope it will be helpful to everyone. If you have any questions, please leave me a message and the editor will reply to everyone in time. Thank you very much for your support for my website!