Recently, I wrote a mobile terminal project, but it is very inconvenient to manually Upload the server every time we build. After all, the process of deleting folders and dragging and uploading is too repetitive. In line with the principle of not repeating the wheel, I went to Github and found that the Upload plug-ins are quite numerous. However, there are still some differences from their own requirements, many plug-ins are just a single responsibility, only responsible for uploading this thing.

However, if you are only responsible for uploading files without deleting them, more and more files will be uploaded to the server, occupying extra storage costs. WebPack will detect whether related dependencies are changed during the build process. If the hash of the changed files is also changed, new files will be uploaded to the server. Old resources are not overwritten and replaced.

The basic concept

The plug-in for WebPack is based on Tapable, which is a publish-subscribe implementation that broadcasts the plug-in’s lifecycle hooks and executes them when appropriate. At the same time, let the plug-in only pay attention to its own subscription, and keep the plug-in assembly in order.

Tapable exposes three methods:

  • Tap: Synchronous and asynchronous hooks can be registered
  • TapAsync: Registers asynchronous hooks as callbacks
  • TapPromise: Registers asynchronous hooks in the Promise form

The WebPack display requires that we have apply methods when we write the plug-in. The reason for this is that the Apply methods are executed during WebPack execution and the Compiler is injected. Hook events are then subscribed to the Compiler, triggering the subscribed Apply methods at the appropriate time

Take another look at the official sample code

// A JavaScript class.
class MyExampleWebpackPlugin {
  // Define `apply` as its prototype method which is supplied with compiler as its argument
  apply(compiler) {
    // Specify the event hook to attach to
    compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin'.(compilation, callback) = > {
      console.log('This is an example plugin! ');
      console.log(
        'Here's the 'compilation' object which teaches a single build of assets:,
        compilation
      );

      // Manipulate the build using the plugin API provided by webpack
      compilation.addModule(/ *... * /); callback(); }); }}Copy the code

The above plug-in subscribes to the emit asynchronous hook in compiler and, after doing some work, executes the callback() callback

The compilation on the left side of the callback is used to access the resource build information, such as output resources, dependencies, and so on.

Knowing the above information, we looked for the compiler hooks we needed. There are many hooks listed in the document:

  • environment
  • afterEnvironment
  • entryOption
  • .

At the end of the compilation, you’ll see a done hook that executes when the compilation completes.

Here we have almost everything we need to do before the done trigger is triggered

  • Connect to the SSH server and runrm-rf xxThe operation of the
  • Upload the built resources to the xx directory

Plug-in development preparation

The rest of the content is developed in TypeScript, so skip type comments if you don’t have the experience

To make it easy to decouple and reuse files, we create an utils.ts file

// utils.ts
import { NodeSSH } from 'node-ssh';

import { Option } from './typings';

export const isObject = (obj: any): obj is Object= >typeof obj === 'object' && obj;

// Delete the folder
export const removeDir = async (option: Option) => {
  const ssh = new NodeSSH();
  await ssh.connect(option);
  await ssh.execCommand(`rm -rf ${option.to}`);
  await ssh.dispose();
};

// Upload the folder
export const uploadDir = async (option: Option) => {
  const ssh = new NodeSSH();
  await ssh.connect(option);
  awaitssh.putDirectory(option.src! , option.to, {recursive: true});await ssh.dispose();
};
Copy the code

It exposes three methods, delete and upload folders and a method to identify objects. The delete and upload folders above are wrapped based on Node-SSH. If you are interested, you can read the documentation

Plug-in development

The rest of the plug-in development is to obtain the user to fill in some necessary fields, such as password, upload server path, host and other information, combined with the above utils and hooks to complete the upload process

// upload-plugin.ts
import { Compiler, Stats } from 'webpack';
import { isObject, uploadDir, removeDir } from './utils';
import { Option } from './typings';

class UploadPlugin {
  public stats: Stats;

  public option: Option & Record<string.any>;

  public removeDir: boolean;

  constructor(option: Option, remove = true) {
    this.stats = null as unknown as Stats;
    this.option = option;
    this.removeDir = remove;
    this.init();
  }

  init() {
    this.checkOption();
    this.setOption();
  }

  // Check the parameters
  checkOption(option = this.option) {
    if(! isObject(option)) {throw new Error('option Must be an object! ');
    }
    const result = ['to'.'host'].filter((f) = >! option[f]);if (result.length) {
      throw new Error(`The ${result.join(', ')}parameter is required! `);
    }
    if(! option.password && ! option.privateKey) {throw new Error('password and privateKey must have one entry! '); }}// Initialize the default value
  setOption() {
    const option = {
      port: 22.username: 'root'};this.option = { ... option, ... this.option, }; }apply(compiler: Compiler) {
    compiler.hooks.done.tap('upload-plugin'.async (stats) => {
      console.time('time');
      // Get the default information, if SRC does not exist to use webpack configuration directly
      const src = stats.compilation.outputOptions.path;
      this.option.src = this.option.src ?? src;
      if (this.removeDir) {
        await removeDir(this.option);
      }
      await uploadDir(this.option);
      console.timeEnd('time'); }); }}export default UploadPlugin;
Copy the code

The overall code is still very concise, remove the parameter verification part and assign the default value parameter, the rest is to delete the remote folder according to the parameter, and then execute the upload method.

If you’re wondering what Option is, it’s a combination of node-SSH connection information and custom extension fields

// typings.d.ts
export interfaceOption { src? :string;
  to: string; port? :number;
  host: string; username? :string; password? :string; privateKey? :string;
}
Copy the code

The last

The complete code has been uploaded to Github warehouse. If you are interested, you can see more specific information. If it is helpful to you, welcome star.