preface

Before writing the scaffolding of the base operation, behind to see the @vue/ CLI source code, have to say, is really very formal and standardized. Reference @vue/ CLI part of the function, to modify scaffolding.

Effect of chestnuts

The project structure

The structure of the project ChuhC-React is divided into four parts:

  • @chuhc/cliScaffolding command line content, initializing projects through commands, and so on.
  • @chuhc/scriptsThe project compiles and runs the package content, but has not been connected to the deployment process.
  • @chuhc/templateTemplate file.
  • @chuhc/plugin-xxxProject-wrapped plug-ins, for example@chuhc/plugin-typescriptAnd so on.

cli

In the past, I directly obtained the information through some interactive commands of Inquirer, and then used the download-git-repo command to pull the template file corresponding to Github. Although this is relatively simple, it is difficult to maintain multiple templates.

In @vue/cli, multiple plug-ins are combined to form a monolithic template. In many scaffolding roots, an xxx.config.js is exposed. Then, when running the Node command, read the configuration file and perform corresponding operations according to the content of the configuration file, for example, dynamically modify the config using webpack-chain, and finally call toConfig to generate the new Webpack configuration content.

The plug-in determines and generates a PKG

A basic package.json template, in addition to the generic version, private, license, etc., such as name, scripts, dependencies, devDependencies need to be added manually.

Name uses scaffolding to initialize the parameters passed in, while scripts uses @chuhc/scripts to run commands after successfully introducing it.

For prerequisites such as react and react-dom, we can place them directly in dependencies, whereas devDependencies are plugins that the user selects manually during initialization.

Use Inquirer to list plug-ins and let users choose which plug-ins to introduce.

const CHUHC_PLUGIN_CHECK = [
  {
    name: 'Typescript'.value: ['tsx'.'@chuhc/plugin-typescript'],}, {name: 'Less'.value: ['less'.'@chuhc/plugin-less'],},];const { plugins } = await inquirer.prompt([
  {
    type: 'checkbox'.name: 'plugins'.message: 'Do you need these plugins'.choices: CHUHC_PLUGIN_CHECK, // A structure, what do you think is convenient, how to come},]);Copy the code

You can add the plugin to the json devDependencies file in the code above.

Get depends on the latest version

Both react and Plugin above require a version number. Here I use the command line to obtain the latest version and use it as its value. If you run execSync directly, it will block, and the ORA loading will be stuck, so select the promise to run, and terminate the promise by exec callback.

const forEachSetV = (list, obj, key) = > {
  const promises = [];
  const manager = hasCnpm() ? 'cnpm' : 'npm'; // Determine whether to select CNPM or NPM

  list.forEach(item= > {
    if (typeof item === 'object') {
      return forEachSetV(item, obj, key);
    }

    const newPromise = new Promise(res= > {
      exec(`${manager} view ${item} version`.(err, stdout, stderr) = > {
        obj[key][item] = stdout.slice(0, stdout.length - 1);
        res(0);
      });
    });

    promises.push(newPromise);
  });

  return promises;
};

const promise = [
  ...forEachSetV(depe, pkg, 'dependencies'),
  ...forEachSetV(devD, pkg, 'devDependencies')];await Promise.all(promise);
Copy the code

Once you get the version number, fill in the json with other data as the package.json value and create it in the new project directory.

const fs = require('fs-extra'); // fs-extra is an extension of system FS module
const path = require('path');

module.exports = (dir, files) = > {
  Object.keys(files).forEach(name= > {
    const pathName = path.join(dir, name);
    fs.ensureDirSync(path.dirname(pathName)); // Create a new folder if there is no folder
    fs.writeFileSync(pathName, files[name]); // Create a new file
  });
};

writeFileTree(targetDir, {
  'package.json': JSON.stringify(pkg, null.2)});Copy the code

Select the package Management tool

Because the speed of NPM is not ideal, it can be used as a cushion. Check whether yarn or CNPM exists in the current environment. Select YARN or CNPM first. If neither exists, use NPM.

const PM_CONFIG = {
  npm: {
    install: ['install'.'--loglevel'.'error'].// Prints error information
    remove: ['uninstall'.'--loglevel'.'error'],},yarn: {
    install: [].remove: ['remove'],}}; PM_CONFIG.cnpm = PM_CONFIG.npm;module.exports = class PackageManager {
  constructor({ pkgName }) {
    this.pkgName = pkgName;

    if (hasYarn()) {
      this.bin = 'yarn';
    } else if (hasCnpm()) {
      this.bin = 'cnpm';
    } else {
      this.bin = 'npm'; }}// encapsulates the run command function
  runCommand(command, args = []) {
    const _commands = [this.bin, ... PM_CONFIG[this.bin][command], ... args]; execSync(_commands.join(' '), { stdio: [0.1.2]}); }install() {
    try {
      this.runCommand('install'['--offline']); // Offline means to pull the cache first, if not, to pull the server
    } catch (e) {
      this.runCommand('install'); // Report an error}}git() {
    try {
      execSync('git init');
      return true;
    } catch (e) {
      return false; }}};Copy the code

To check whether the YARN and CNPM environments exist, you can check version to check whether the yarn and CNPM environments can be successfully executed. If the yarn and CNPM environments exist, the yarn and CNPM environments do not exist.

const judgeEnv = name= > {
  const envKey = `_has${name[0].toUpperCase()}${name.slice(1)}`; // Save the result

  if(_env[envKey] ! = =null) {
    return _env[envKey];
  }

  try {
    execSync(`${name} --version`, { stdio: 'ignore' }); // Do not print information

    return (_env[envKey] = true);
  } catch (e) {
    return (_env[envKey] = false); }};const hasYarn = judgeEnv.bind(this.'yarn');

const hasCnpm = judgeEnv.bind(this.'cnpm');
Copy the code

Then install the dependencies using the install method and pass some parameters to @chuhc/template to copy some base templates.

scripts

Because it’s a multi-page project, scripts does the following:

  • throughglobTo match the entry and then treat it asentryDynamically pass in, and dynamically pass in multiplehtml-webpack-plugintoplugins.
  • By reading thechuhc.config.jsFile, to dynamically modifywebpackConfigure the content and invoke the corresponding plug-in.
  • And then finally produce the finalwebpackConfiguration file, passed towebpackTo compile, run, package, etc.

Match the entrance

The matching entry mainly uses glob to match, and only meets the matching requirements can it be used as an entry. Then, through the matching information, the corresponding entry content and plugin content are generated and passed to the Webpack configuration file.

const SRC = './src/**/index.? (js|jsx|ts|tsx)'; 
/** * get webpack entry */
const getEntries = () = > {
  if (entries) return entries;
  entries = {};

  const pages = glob.sync(SRC);
  pages.forEach(page= > { // Pass through the entry
    const dirname = path.dirname(page);
    const entry = path.basename(dirname);
    entries[entry] = page;
  });
  return entries;
};

/**
 * get pages info
 * @param {Boolean} isProd* /
const getPages = isProd= > {
  const plugins = [];
  let entries = getEntries();

  Object.keys(entries).map(dirname= > { // Pass plugin
    plugins.push(
      new HtmlWebpackPlugin({
        chunks: isProd ? ['libs', dirname] : [dirname],
        filename: `. /${dirname}/index.html`.template: path.join(__dirname, './template/index.html')})); });return plugins;
};
Copy the code

Chain configuration config

Webpack-chain is recommended for chained configurations, as is @vue/ CLI. Because we already have some basic configuration content, we can merge our existing configuration objects into a configuration instance using config.merge.

However, direct conversion is not supported. We need to manually convert some configuration content, such as module. Plugins do not support plugins that are already new, so my solution is to skip the merge of plugins and finally merge config.toconfig () and plugins into the final configuration object using webpack-merge.

const Config = require('webpack-chain');
const chuhcConfig = require(`${process.cwd()}/chuhc.config.js`); // Read the configuration file in the root directory
const { setBaseConfig } = require('./util/merge'); // Merge existing profile objects into the configuration instance
const BASE = require('./config/webpack.base'); // Configure the base object
const DEVE = merge(BASE, require('./config/webpack.deve')); // Configure object deve
const PROD = merge(BASE, require('./config/webpack.prod')); // Configure object prod

const config = new Config();

// This is just an example of how plugins can be used to do many other things
const handleChuhcConfig = ({ plugins } = {}) = > {
  // to do sth.
  if (plugins) {
    plugins.forEach(plugin= > {
      require(plugin[0])(config, plugin[1]); }); }};const getConfig = isDeve= > {
  config.clear(); // Clear the configuration

  setBaseConfig(isDeve ? DEVE : PROD, config);
  handleChuhcConfig(chuhcConfig);

  return merge(config.toConfig(), {
    plugins: isDeve ? DEVE.plugins : PROD.plugins
  }); // Finally merge
};
Copy the code

Compile operation

After obtaining webpack Config, you can call the corresponding function, compile, run, pack and so on, depending on whether the dev command or build command is used. (Also, according to program.command)

/ run/dev
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const { getDeveConfig } = require('./config');

module.exports = () = > {
  const DEVE_CONFIG = getDeveConfig();
  const { devServer } = DEVE_CONFIG;
  const compiler = webpack(DEVE_CONFIG);
  const server = newWebpackDevServer(compiler, { ... devServer }); server.listen(devServer.port); };Copy the code
// build
const webpack = require('webpack');
const { getProdConfig } = require('./config');
const logSymbols = require('log-symbols');

module.exports = () = > {
  const PROD_CONFIG = getProdConfig();
  const compiler = webpack(PROD_CONFIG);

  compiler.run((err, stats) = > {
    if (err) {
      Error message received in callback.
      console.error(err);
    } else {
      console.log(logSymbols.success, 'Packed successfully! '); }}); };Copy the code

template

Template is used to determine whether to copy the corresponding file based on the parameters passed in, and modify the corresponding file content and suffix according to options. The code is too boring to post.

plugin-xx

Plugin can do a lot of things, I just chain modify webpack configuration information. This is just one feature, there are many more such as: write your own Webpack plugin/loader to pass in, to do some other things.

// example
module.exports = config= >{['.tsx'.'.ts'].forEach(item= > config.resolve.extensions.add(item));

  config.module
    .rule('js')
    .test(/\.(js|ts|tsx|jsx)$/)
    .use('babel-loader')
    .tap(options= > {
      options.presets.push('@babel/preset-typescript');
      return options;
    });
};
Copy the code

github

The complete code

PS: From today on, keep learning. A while ago, I didn’t want to do anything. Every day after work, I just wanted to lie down and be confused.