This is the 137th original article without water, if you want to get more original good articles, please search the public account to follow us. This article was first published in the front blog of political research cloud: Lerna operation process analysis

preface

With the development of front-end components, package libraries and other engineering systems, the relationship between business components and tool libraries becomes more and more complex. It is very easy to encounter multiple warehouses and interdependent libraries. As a result, maintenance is extremely difficult and the package delivery process is very tedious, which greatly limits the development efficiency of front-end students.

At the moment, there is a new way of project management – Monorepo. A warehouse manages multiple projects.

MultiRepo is a common project management approach today. But some scenarios are not applicable and there are problems.

  • Multiple business components, interdependence, and inability to reuse
  • The package sending process is complicated and version management is painful

Now we have lerna.js

Introduction to the

Lerna (lerna) is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

Lerna is an optimized multi-package project management tool based on Git + NPM.

What projects are using Ta?

  • Vue Cli github.com/vuejs/vue-c…

  • create-react-app github.com/babel/babel

  • Mint – UI github.com/ElemeFE/min…

    .

knowledge

By reading this article, you will learn the following:

Use and Practice

Basic instructions

Lerna’s basic common instructions are not the focus of this article. The lerna documentation is here.

Below is the structure directory, etc.

Working with workspaces

/ / package. Add json
"workspaces": ["packages/*"
]
/ / lerna. Add json
 "useWorkspaces":true."npmClient": "yarn".// Once configured, all dependencies are installed in the outermost node_modules and soft link mode is supported
 // NPM 7.x also supports working areas
Copy the code

The learning process involves looking at the implementation process and running process. Let’s take a look at some of the code in Lerna and hopefully you’ll learn a lot.

The principle of analyzing

Github clone Lerna

Look at the catalog

The initialization process of the instruction

The scaffold entry file is located at /core/lerna/cli.js

The core/lerna/cli. Js entrance

#! /usr/bin/env node

"use strict";

/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
// Determine whether the file is in a local package, as described below
if (importLocal(__filename)) {
  require("npmlog").info("cli"."using local version of lerna");
} else {
// Enter the actual entry to execute the code
  require(".")(process.argv.slice(2)); // [node, lerna, command]
}
Copy the code

The file in Figure 1 and the code entry only executes a judgment statement, which aims to preferentially use lerNA codes in the local environment when lerNA exists in both the local environment and the global environment of the project

  • Import-local A library of methods used to determine whether a package is local
  • Require (“.”) imports the current directory of index.js and passes in the instruction execution code (process.argv -> [node, lerna, instruction])

The core/lerna/index. Js initialization

/** omit the same code */

// Import @lerna/cli
const cli = require("@lerna/cli");

/ /... Omit the same instruction import
// Import the publish directive file
const publishCmd = require("@lerna/publish/command");
const pkg = require("./package.json");

module.exports = main;

// The final export method
function main(argv) {
  const context = {
    lernaVersion: pkg.version,
  };

  return cli()
  / /... omit
    .command(publishCmd)
    .parse(argv, context); // Parse injection instructions & parameters (version numbers)
}
Copy the code

Coming to this code, figure 2 and the code actually do these things

  • Initialize the import package (“@lerna/cli”) — CLI instance
  • Import the required instruction file
  • Register commands using the command method of the CLI instance
  • Parse (argv, context) executes parse injection instructions and parameters (version number)

The Cli | | instructions into the module classification, whether in business or in the open source library, is a good way of division

Core /cli/index.js global instruction initialization

const dedent = require("dedent"); // remove blank lines
const log = require("npmlog");
const yargs = require("yargs/yargs");
const { globalOptions } = require("@lerna/global-options");

module.exports = lernaCLI;

function lernaCLI(argv, cwd) {
  const cli = yargs(argv, cwd);

  return globalOptions(cli)
    .usage("Usage: $0 <command> [options]")
    .demandCommand(1."A command is required. Pass --help to see all available commands and options.") // Number of expected commands
    .recommendCommands() // The command is recommended
    .strict()  // Strict mode
    .fail((msg, err) = > {
     / /... omit
    })
    .alias("h"."help") / / alias
    .alias("v"."version")
    .wrap(cli.terminalWidth()) / wide/high
	  .epilogue(dedent` When a command fails, all logs are written to lerna-debug.log in the current working directory. For more information, find our manual at https://github.com/lerna/lerna `);  / / the end
}
Copy the code

Looking at Figure 3 global directive initialization, we see that the global directive accepts the incoming instance and also supports the registration of the directive. Obviously this also exports a modified CLI instance (single instance)

  • The registration of the directive is managed using the YARgs package (YARgs is not the focus of this article).
  • The global directive registers the return instance
  • Config is the basic configuration group etc
  • Export the instance to the **core/lerna/index.js ** call

We go back to the core/lerna/index.js file and use the command method to register the directive and pass in the imported directive file.

Commands/business command registration

You can see in Figure 4 that the Commands file package has registration files for all lerna directives, each with command-.js and index.js embedded in it

**core/lerna/index.js ** imports command-js from that directory (same entry logic executes index.js from that directory).

**command. Js includes yargs command, **aliases, describe, Builder (parameter operation before execution), handler (command execution logic)

Take the list directive

  • The method that executes the logic of the instruction is in index.js
  • Inheriting Command to initialize an instruction
  • Constructor executes the Initialize and execute methods in the parent class
const { Command } = require("@lerna/command");
const listable = require("@lerna/listable");
const { output } = require("@lerna/output");
const { getFilteredPackages } = require("@lerna/filter-options");

module.exports = factory;

function factory(argv) {
  return new ListCommand(argv);
}

class ListCommand extends Command {
  get requiresGit() {
    return false;
  }

  initialize() {
    let chain = Promise.resolve();

    chain = chain.then(() = > getFilteredPackages(this.packageGraph, this.execOpts, this.options));
    chain = chain.then((filteredPackages) = > {
      this.result = listable.format(filteredPackages, this.options);
    });

    return chain;
  }

  execute() {
    // piping to `wc -l` should not yield 1 when no packages matched
    if (this.result.text.length) {
      output(this.result.text);
    }

    this.logger.success(
      "found"."%d %s".this.result.count,
      this.result.count === 1 ? "package" : "packages"); }}module.exports.ListCommand = ListCommand;
Copy the code

Core /command/index.js Command Class for all instructions

const { Project } = require("@lerna/project");
// Omit most of the fault tolerance and log
class Command {
  constructor(_argv) {
    const argv = cloneDeep(_argv);

    // "FooCommand" => "foo"
    this.name = this.constructor.name.replace(/Command$/."").toLowerCase();

    // composed commands are called from other commands, like publish -> version
    this.composed = typeof argv.composed === "string"&& argv.composed ! = =this.name;

    // launch the command
    let runner = new Promise((resolve, reject) = > {
      // run everything inside a Promise chain
      / / asynchronous chain
      let chain = Promise.resolve();

      chain = chain.then(() = > {
        this.project = new Project(argv.cwd);
      });
      // Configuration, environment initialization, etc
      chain = chain.then(() = > this.configureEnvironment());
      chain = chain.then(() = > this.configureOptions());
      chain = chain.then(() = > this.configureProperties());
      chain = chain.then(() = > this.configureLogging());
      chain = chain.then(() = > this.runValidations());
      chain = chain.then(() = > this.runPreparations());
      // Finally execute the logic
      chain = chain.then(() = > this.runCommand());

      chain.then(
        (result) = > {
          warnIfHanging();

          resolve(result);
        },
        (err) = > {
          if (err.pkg) {
            // Cleanly log specific package error details
            logPackageError(err, this.options.stream);
          } else if(err.name ! = ="ValidationError") {
            // npmlog does some funny stuff to the stack by default,
            // so pass it directly to avoid duplication.
            log.error("", cleanStack(err, this.constructor.name));
          }

          // ValidationError does not trigger a log dump, nor do external package errors
          if(err.name ! = ="ValidationError" && !err.pkg) {
            writeLogFile(this.project.rootPath);
          }

          warnIfHanging();
          // error code is handled by cli.fail()reject(err); }); });/ /... Omit some code
  }



  runCommand() {
    return Promise.resolve()
    	// Command initialization
      .then(() = > this.initialize())
      .then((proceed) = > {
        if(proceed ! = =false) {
          // Command execution
          return this.execute();
        }
        // early exits set their own exitCode (if non-zero)
      });
  }
	// Throw an error if the subclass does not exist
  initialize() {
    throw new ValidationError(this.name, "initialize() needs to be implemented.");
  }

  execute() {
    throw new ValidationError(this.name, "execute() needs to be implemented."); }}module.exports.Command = Command;
Copy the code

The primary concern in Class is the constructor logic, as shown in Figure 5, and the code. As stated above, each subinstruction class executes the Initialize and execute methods. Let’s reorganize

  • Create promise.resolve () asynchronous Chain.
  • Initialize the global configuration, parameters, and environment
  • Execute the runCommand method
  • RunCommand calls initialize and execute (if the subclass does not have one, the parent class will throw an exception)

Using template mode, the sub – instruction logic unified template. This is the basic execution flow. In this Class, the logic for initializing the instruction, executing the instruction, and so on is cleverly registered in the asynchronous tasks of the Promise.

  • The execution logic of the commands is later than the Cli synchronization code. (Does not affect Cli code execution)
  • All exception errors can be caught uniformly

Through the above learning, we almost understand the process of an instruction input -> parse -> register -> execute -> output of LERNA.

What does import-local do in the first step of scaffolding initialization?

Scaffolding initialization process

Import-local is used to obtain whether the NPM package exists locally (in the current working area). It is used to determine whether the globally installed package has been installed locally, and it is used in most CLI such as Webpack-CLI.

const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');

module.exports = filename= > {
  / / '/ Users/NVM/versions/node/v14.17.3 / lib/node_modules/lerna' global folder
  const globalDir = pkgDir.sync(path.dirname(filename));
  const relativePath = path.relative(globalDir, filename); // 'cli.js'
  const pkg = require(path.join(globalDir, 'package.json'));
  / / '/ Users/Desktop/person/lerna - demo/node_modules/lerna/cli. Js' / / local files
  const localFile = resolveCwd.silent(path.join(pkg.name, relativePath)); 
  / / '/ Users/Desktop/person/lerna - demo/node_modules'/node_modules/local files
  const localNodeModules = path.join(process.cwd(), 'node_modules'); 
  constfilenameInLocalNodeModules = ! path.relative(localNodeModules, filename).startsWith('.. ') &&
    // On Windows, if `localNodeModules` and `filename` are on different partitions, `path.relative()` returns the value of `filename`, resulting in `filenameInLocalNodeModules` incorrectly becoming `true`.
    path.parse(localNodeModules).root === path.parse(filename).root;

  // Use `path.relative()` to detect local package installation,
  // because __filename's case is inconsistent on Windows
  // Can use `===` when targeting Node.js 8
  // See https://github.com/nodejs/node/issues/6624

  // Import using local packages
  return! filenameInLocalNodeModules && localFile && path.relative(localFile, filename) ! = =' ' && require(localFile);
};
Copy the code

In the last line, you can see that the most important is to resolve the specified NPM package exists in the global and NPM folder, path. To determine whether require() is local or global.

Question & Contrast

Before comparing and examining the issues, let’s focus on the advantages of Monorepo’s single-warehouse, multi-project management model.

Do you encounter any of the following problems in front-end work?

Question 1:

Xiao Ming found the same business logic in Xiao Hong’s project

A: I chose to copy the code

B: I choose to package it into NPM package for multi-project reuse

Obviously, approach A is not an option to solve this problem, and is not at all compatible with the code design philosophy of the application.

Most of the students will agree and I will choose B

What if this NPM package is discovered in subsequent iterations and package dependencies are upgraded and released?

Or perhaps there are most of these scenarios in the business, where each package is not managed uniformly and most of the time is spent upgrading and publishing between package dependencies. And iterations of the respective packages.

You may delete only one line of code, but you have to perform the entire process for every NPM package that depends on that package.

Question 2:

Updates to NPM packages are inevitable during development, and when you do, you will need to tag and update the impact surface of the current package. Is it a minor change, or is it an update that doesn’t work with a larger VERSION of the API? All of these actions can lead to problems in developing projects where dependencies are not updated in a timely manner and tag errors occur.

Strengths & Weaknesses

For now, Monorepo solves the time-saving problem of dependency change upgrading between multiple warehouses and batch package management.

So the common existence and dependence of this model in the open source community on subcontracting, but independent projects (NPM, scaffolding, etc.)

However, lerna’s multi-packet management also has its disadvantages

  • Debugging between dependencies is complex
  • The Changelog information is incomplete
  • Lerna itself does not support the workspace concept and requires other tools
  • CI customization cost is large

otherMultiRepo scheme

As can be seen from the picture

PNPM pays more attention to package management (like download, stability and accuracy, etc.), while LERna pays more attention to specification of package release process.

The scenarios are slightly different.

expand

The import – local parsing

In Figure 6 and the code below, it is clear that resolve-cwd and pkg-dir are the primary toolkits for implementing import-local

  • Resolve-cwd resolves the path to modules like require.resolve (), but from the current working directory.
  • Pkg-dir finds node.js projects or NPM packages from the root directory

Resolve path sources using the resolve-from toolkit in resolve-cwd

const path = require('path');
const Module = require('module');
// Omit some code
const fromFile = path.join(fromDirectory, 'noop.js');
 // '/Users/Desktop/home/person/lerna-demo/noop.js'

const resolveFileName = () = > Module._resolveFilename(moduleId, {
    id: fromFile,
    filename: fromFile,
    paths: Module._nodeModulePaths(fromDirectory)
});
Copy the code
  • Use the native module’s two native apis: module. _resolveFilename and module. _nodeModulePaths
  • Module._nodeModulePaths Deduce that there may be an array of paths for the node/ JS/JSON package files
  • In the module-_resolvefilename method, the first check is to see if the local Module has the Module.

The attributes of the module object contain

  • module.id
  • module.filename
  • module.loaded
  • module.parent
  • module.children
  • module.paths

Module is one of the core methods to implement require() and hot loading.

Part of the implementation can refer to ruan Yifeng teacher’s require() source code interpretation

Use the find-up toolkit in pkg-dir to go up the global package folder

const locatePath = require('locate-path');
const stop = Symbol('findUp.stop');

module.exports.sync = (name, options = {}) = > {
  let directory = path.resolve(options.cwd || ' ');
  const {root} = path.parse(directory);
  const paths = [].concat(name);

  const runMatcher = locateOptions= > {
    if (typeofname ! = ='function') {
      return locatePath.sync(paths, locateOptions);
    }
    const foundPath = name(locateOptions.cwd);
    if (typeof foundPath === 'string') {
      return locatePath.sync([foundPath], locateOptions);
    }
    return foundPath;
  };
  // eslint-disable-next-line no-constant-condition
  while (true) {
    constfoundPath = runMatcher({... options,cwd: directory});

    if (foundPath === stop) {
      return;
    }
    if (foundPath) {
        return path.resolve(directory, foundPath);
    }
    if (directory === root) {
      return; } directory = path.dirname(directory); }};Copy the code
  • The package.json file exists in the current CWD lookup
  • So locatePath. Sync takes an array of the file paths to find and the CWD paths to execute
  • Resolve (directory, foundPath) through the while loop until the return path.resolve(directory, foundPath) is found;

What are soft links

fs.symlink(target, path[, type], callback) Node/symlink

Target < string > | < Buffer > | < URL > / / the target file path < string > | < Buffer > | < URL > / / create soft chain corresponding address type < string >Copy the code

The API creates a link with a path that points to Target. The type parameter is only available on Windows and is ignored on other platforms. Can be set to dir, file, or function. If the type parameter is not set, Node.js will automatically detect the type of target and use file or dir.

If target does not exist, ‘file’ will be used. Connection points on Windows require that the destination path be an absolute path. When ‘function’ is used, the target argument is automatically normalized to an absolute path.

conclusion

  • From the process design of Lerna, we can find that lerna has split and combined every executable Node program. In your own code design, you can expect to encounter messy code as well.

Do you ignore it now, or do you sort the code from “miscellaneous” -> “split” -> “join”

  • Second, we see that in LERNA, singletons are used to register instructions. In registering instructions, face objects and template patterns are used to separate the common initialization logic. In the execution process of instructions, it is all about the execution of micro-tasks, which are all design ideas and design modes that can be learned.
  • Finally, the comparison of other MultiRepo schemes shows that the capabilities given by the tool have their pros and cons, neither good nor bad, but more suitable.

reference

  • Lerna document
  • Ruan yifeng teacher require() source code interpretation

Recommended reading

The Decorator Decorator

Analysis of VNode and DIff algorithm in Snabbdom

How to use SCSS to achieve one key skin change

Why is index not recommended as key in Vue

Open source works

  • Political cloud front-end tabloid

Open source address www.zoo.team/openweekly/ (wechat communication group on the official website of tabloid)

  • Item selection SKU plug-in

Open source addressGithub.com/zcy-inc/sku…

, recruiting

ZooTeam, a young passionate and creative front-end team, belongs to the PRODUCT R&D department of ZooTeam, based in picturesque Hangzhou. The team now has more than 60 front-end partners, with an average age of 27, and nearly 40% of them are full-stack engineers, no problem in the youth storm group. The members consist of “old” soldiers from Alibaba and NetEase, as well as fresh graduates from Zhejiang University, University of Science and Technology of China, Hangzhou Electric And other universities. In addition to daily business docking, the team also carried out technical exploration and practice in material system, engineering platform, building platform, performance experience, cloud application, data analysis and visualization, promoted and implemented a series of internal technical products, and continued to explore the new boundary of front-end technology system.

If you want to change what’s been bothering you, you want to start bothering you. If you want to change, you’ve been told you need more ideas, but you don’t have a solution. If you want change, you have the power to make it happen, but you don’t need it. If you want to change what you want to accomplish, you need a team to support you, but you don’t have the position to lead people. If you want to change the pace, it will be “5 years and 3 years of experience”; If you want to change the original savvy is good, but there is always a layer of fuzzy window… If you believe in the power of believing, believing that ordinary people can achieve extraordinary things, believing that you can meet a better version of yourself. If you want to be a part of the process of growing a front end team with deep business understanding, sound technology systems, technology value creation, and impact spillover as your business takes off, I think we should talk. Any time, waiting for you to write something and send it to [email protected]