Develop a Webpack from scratch: Part 1

In this section, the target

Implement Loader support.

The concept is introduced

Loader is a JS module that exports a function, so loader is essentially a function. The Loader function takes a string or buffer as an argument, performs some operations on the content, and returns the processed string.

Implement your own loader

Now that we know that loader is a function, we can also write our own loader. Here are two simple examples.

strip-slc-loader

Strip-slc-loader is a loader responsible for removing single-line comments.

  • Enter: js source code string
  • Output: Remove all single-line commented JS code strings
module.exports = content= > content.replace(/\/\/.*\n/g.' ')
Copy the code

mock-babel-loader

Mock -babel-loader is responsible for converting ES6 code into ES5 code. Here we use the Syncing API transformSync from Babel’s core library.

  • Enter: ES6 source code string
  • Output: ES5 code string
const babel = require('@babel/core')

module.exports = content= > {
  return babel.transformSync(content, {
    presets: ['@babel/preset-env']
  }).code
}
Copy the code

Loader features

  • Loaders can be synchronous or asynchronous. The above two examples are synchronous loaders (asynchronous loaders are not considered for the time being).
  • Loader supports chain processing, that is, multiple Loaders can process a file sequentially. One Loader will receive the processing result of the previous loader as a parameter, and the direction of data flow is from right to left according to the configuration sequence.
  • Loader runs in Nodejs;
  • And so on.

There are many loader features, but we only use the above ones here, more can be learned by yourself.

Now we are ready to implement Loader support in mini-WebPack.

Use effect

Module. rules can be used to configure which loaders are used to process different types of files.

bundle({
  entry: '.. /example/entry.js'.output: {
    path: 'dist'.filename: 'bundle.js'
  },
  module: {
    rules: [{
      test: /\.js$/.use: [{
          loader: 'mock-babel-loader'
        },
        {
          loader: 'strip-slc-loader'}]}]}})Copy the code

Strip-slc-loader removes single line comments from js files and mock-babel-loader converts ES6 syntax to ES5 syntax.

implementation

Then we’ll modify our mini-Webpack source code to support loader.

bundle

First, our bundle functions should add support for module options, including the following changes.

const bundle = options= > {
  const {
    entry,
    output,
+    module,
  } = options

  // ...

  // Pass module.rules to the createModule
-  const entryModule = createModule(id++, path.resolve(__dirname, entry))
+  const entryModule = createModule(id++, path.resolve(__dirname, entry), module.rules)

  // Passing module.rules to createModules is intended to be passed to the createModule
-  const modules = [entryModule].concat(createModules(id, entryModule, module.rules))
+  const modules = [entryModule].concat(createModules(id, entryModule, module.rules))

  // ...
}
Copy the code

createModule

In the createModule function, the file contents are passed to the Loader for processing.

- const createModule = (id, path) = >{+const createModule = (id, path, rules) = > {
  let content = fs.readFileSync(path, {
    encoding: 'utf-8'
  })
  const dependencies = []

  // With the addition of the Loader mechanism, the createModule theoretically supports other types of files
  Parser can only handle JS files
+  if (/\.js$/.test(path)) {
    const ast = parser.parse(content, {
      sourceType: 'module'
    })

    traverse(ast, {
      ImportDeclaration({ node }) {
        dependencies.push(node.source.value)
      }
    })
+  }

  // Pass the file contents to loader
  // applyLoaders returns the processed content as a string
+  content = applyLoaders(path, rules, content)

  return {
    id,
    content,
    dependencies,
    filename: path,
    mapping: {}}}Copy the code

applyLoaders

ApplyLoaders is the core code responsible for:

  1. Check whether there is an appropriate loader in the configuration to process the current file module;
  2. Use loader processing modules from right to left in the configuration sequence, and the parameters received by the current Loader are the processing results of the previous loader.
  3. Returns the contents of the processed file;
  4. If no loader processes the current file, a message is displayed.
const applyLoaders = (modulePath, rules, content) = > {
  // hit is used to indicate whether there is an appropriate loader to process the file
  // Give a hint if not
  let hit = false
  const res = rules.reduce((content, rule) = > {
    if (rule.test.test(modulePath)) {
      hit = true
      // Loaders are processed from right to left
      return rule.use.reduceRight((content, {loader}) = > {
        // Import the loader function and pass in the source code
        // Return the processed source code to the next loader
        // I put all my loader modules in the loaders folder
        // So go to this folder and call the loader function and pass in the file contents
        return require(path.resolve('loaders', loader))(content)
      }, content)
    }
  }, content)

  hit || console.log(`${path}: You may need an appropriate loader to handle this file type.`);
  return res
}
Copy the code

summary

At this point our Mini-Webpack is complete with loader support, run the NPM run demo in the project to see the packaging effect.

The warehouse address

Github.com/suukii/mini…