This paper links: jsonz1993. Making. IO / 2018/05 / cre…

Series 2 Address

Work has stabilized recently, not so much overtime… So you start to have free time to learn some front-end stuff

One of the company’s bigwigs had written a scaffolding like create-React-App to create the company’s projects. Create-react-app (create-react-app, create-React-app)

We do not see the source code is afraid to see, now so good projects are open source, plus a variety of IDE support is very good, directly hit a breakpoint debugging, it is easy to see about. You can also use this approach to other open source projects

Emmmm writes for the first time to accept any ridicule

Quickly understand

For those who want a quick look, just browse this section

Create-react-app uses Node to run the package installation process and file template demo into the appropriate directory.

It can be simply divided into the following steps:

  • Checking the Node Version
  • Do some initialization for command line processing, such as typing-helpThe help content is displayed
  • Check whether the project name is entered. If yes, install the package according to the parameters. The default installation mode is YARN.yarn add react react-dom react-scripts
  • Modify the installed dependencies in package.json from the exact version16.0.0Change to ^ Upward compatible version^ 16.0.0And addstart.buildEtc startup script
  • copyreact-scriptsUnder thetemplateTo the target file. It haspublic.srcEtc folder, in fact, is a simple working demo
  • END~

Continue to look at the small partners can follow the step by step to understand the implementation logic, the precedent line explains the environment version:

Create-react-app v1.1.4 macOS 10.13.4 node v8.9.4 NPM 6.0.0 YARN 1.6.0 vsCode 1.22.2Copy the code

Project initialization

Go to Github and pull the project code, then switch to the specified tag

  • git clone https://github.com/facebook/create-react-app.git
  • Git checkout v1.1.4
  • yarn// This step can be skipped if breakpoint debugging is not required

If the yarn version is too low, a series of errors will be reported. The previous version is 0.x, and the upgrade to 1.x will be ok

Let’s use root instead of the project root directory to make things easier to understand

First we open the project and see a bunch of configuration files and two folders: ESLint configuration files, Travis deployment configuration, YARN configuration, update log, open source declaration, etc… We don’t have to look at all of these, so where is the core source code we want to look at

Highlight: If the project doesn’t know where to start, where to start with a package.json file

root/package.json

{
  "private": true."workspaces": [
    "packages/*"]."scripts": {
    "start": "cd packages/react-scripts && node scripts/start.js",},"devDependencies": {},"lint-staged": {}}Copy the code

Json: NPM script, development dependencies, and commit hooks. The rest of the workspaces we need to focus on are “Packages /*”, so we’ll focus on packages

Root /packages/create-react-app/package.json root/packages/create-react-app/package

packages/create-react-app/package.json

{
  "name": "create-react-app"."version": "1.5.2"."license": "MIT"."engines": {},"bugs": {},"files": [
    "index.js"."createReactApp.js"]."bin": {
    "create-react-app": "./index.js"
  },
  "dependencies": {}}Copy the code

There is no workspaces item at this time, we can look at bin. The function of bin is to map commands to executable files, see package Document for details

When create-react-app is installed globally, Run create-react-app my-react-app to run packages/create-react-app/index.js my-react-app

Finally found the source code entry, for simple source code we can directly read, for more complex or want to see the execution of each line of code when the variables are what the value of the case, we will use IDE or other tools to debug the code breakpoint.

Configuring breakpoint debugging

Those familiar with vscode or node debugging can skip the start breakpoint and read the source code

vscode debug

For vscode users, debugging is very simple, click on the bug icon in the sidebar, click on Settings and change the value of “program” directly, then click the green arrow in the upper left corner to run, if you want to break at a certain point, For example, create-react-app/index.js line39 breakpoint, just click to the left of the line

Launch. The json configuration

{
  "version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "Start program"."program": "${workspaceFolder}/packages/create-react-app/index.js",}}]Copy the code

The node debugging

If you don’t use vscode or are used to chrome-devtool, you can run the node command directly. Debug in Chrome. First make sure node is version 6 or older. Then run node –inspect-brk Packages /create-react-app/index.js in the project root directory and type in the Chrome address bar Chrome ://inspect/#devices Then you can see the script we are going to debug for Node Chrome-devtool

The terminal starts node debugging

Start reading the source code at breakpoint

packages/create-react-app/index.js Github file portal

packages/creat-react-app/index.js

./createReactApp

packages/create-react-app/createReactApp.js Github file portal

Follow our breakpoint to Createreactapp.js. The file is 750 lines long and at first glance looks like a lot, with a dozen dependencies introduced in the header, but don’t be alarmed, most of these high-quality open source projects are full of comments and error-friendly messages.

Try copying the code to another JS file, and then don’t look at the dependencies in front of it, and then go to NPM to check what is used. Don’t get sidetracked by looking at one dependency after another without seeing the core code. Then I read a section of the code and delete that section of the code. For example, IF I read 200 lines, I delete the first 200 lines so that the remaining 500 lines don’t look so guilty. Of course, it is recommended to debug reading with breakpoints, logic will be more clear.

First of all, we will not pay attention to the dependencies in the header of the file, and we will check them later

const validateProjectName = require('validate-npm-package-name');
const chalk = require('chalk');
const commander = require('commander');
const fs = require('fs-extra');
const path = require('path');
const execSync = require('child_process').execSync;
const spawn = require('cross-spawn');
const semver = require('semver');
const dns = require('dns');
const tmp = require('tmp');
const unpack = require('tar-pack').unpack;
const url = require('url');
const hyperquest = require('hyperquest');
const envinfo = require('envinfo');
Copy the code

Commander Command line processor

Following our breakpoint, the first line of code to be executed is L56

const program = new commander.Command(packageJson.name)
  .version(packageJson.version) ${packagejson.version} ${packagejson.version}
  .arguments('<project-directory>') / / here in a < > project - project directory said - directory is required
  .usage(`${chalk.green('<project-directory>')} [options]`) 
      
  .action(name= > {
    projectName = name;
  }) // Get the first argument passed in by the user as projectName **
  .option('--verbose'.'print additional logs') // option is used to configure 'create-react-app -[option]'. For example, if the user parameter contains --verbose, program.verbose = true is automatically set.
  .option('--info'.'print environment debug info') // This parameter will be used later to print version information for environment debugging
  .option(
    '--scripts-version <alternative-package>'.'use a non-standard version of react-scripts'
  )
  .option('--use-npm')
  .allowUnknownOption()
   // on('option', cb) enter create-react-app --help to automatically perform the following operations and output help
  .on('--help', () = > {console.log(`    Only ${chalk.green('<project-directory>')} is required.`);
    console.log();
    console.log(
      `    A custom ${chalk.cyan('--scripts-version')} can be one of:`
    );
    console.log(`      - a specific npm version: ${chalk.green('0.8.2')}`);
    console.log(
      `      - a custom fork published on npm: ${chalk.green(
        'my-react-scripts'
      )}`
    );
    console.log(
      `      - a .tgz archive: ${chalk.green(
        'https://mysite.com/my-react-scripts-0.8.2.tgz'
      )}`
    );
    console.log(
      `      - a .tar.gz archive: ${chalk.green(
        'https://mysite.com/my-react-scripts-0.8.2.tar.gz'
      )}`
    );
    console.log(
      ` It is not needed unless you specifically want to use a fork.`
    );
    console.log();
    console.log(
      ` If you have any problems, do not hesitate to file an issue:`
    );
    console.log(
      `      ${chalk.cyan(
        'https://github.com/facebookincubator/create-react-app/issues/new'
      )}`
    );
    console.log();
  })
  .parse(process.argv); // Parses passed arguments can be ignored
Copy the code

The commander dependency is used here, so we can go to NPM to find out what it does. The complete Solution for Node.js command-line interfaces Inspired by Ruby’s Commander.API Documentation translates to a complete solution for the Node.js command line interface. Github Portal.

Check whether projectName is passed

if (typeof projectName === 'undefined') {
  if (program.info) { // If the command line has the --info argument, print the react,react-dom,react-scripts versions and exit
    envinfo.print({
      packages: ['react'.'react-dom'.'react-scripts'].noNativeIDE: true.duplicates: true}); process.exit(0); }... Some error messages are printed here... process.exit(1);
}
Copy the code

Action (name => {projectName = name; }. Determine if there is no input, simply do some information, and then terminate the program. If –info is passed in, envinfo.print is executed. Check envinfo for NPM. This is a tool that displays system information about the current environment, such as the system version, NPM, etc. React,react-dom,react-scripts, etc. The current version of the package is quite different from the create-React-app version, but it does not affect our use of the ~ envinfo NPM portal

With the vscode debug configuration I provided above, the program should be finished at this point because we didn’t pass the projectName to the script when we started the debug service. Json “args”: [“test-create-react-app”] forget how to set the projectName parameter

{
  "version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "Start program"."program": "${workspaceFolder}/packages/create-react-app/index.js"."args": [
        "test-create-react-app"]]}}Copy the code

The commander parameter is hidden

Proceed to Line140 after judging projectName

const hiddenProgram = new commander.Command()
  .option(
    '--internal-testing-template <path-to-template>'.'(internal usage only, DO NOT RELY ON THIS) ' +
      'use a non-standard application template'
  )
  .parse(process.argv);
Copy the code

As you can see, this is a hidden debug option that gives you a parameter to pass in the template path to the developer for debugging. Don’t screw him over

createApp

createApp(
  projectName,
  program.verbose,
  program.scriptsVersion,
  program.useNpm,
  hiddenProgram.internalTestingTemplate
);
Copy the code

CreateApp is then called, passing in parameters that mean: the project name, whether to output additional information, the version of the script passed in, whether to use NPM, and the template path to debug. Next step into the function body to see what createApp actually does.

function createApp(name, verbose, version, useNpm, template) {
  const root = path.resolve(name);
  const appName = path.basename(root);

  checkAppName(appName); // Check the validity of the incoming project name
  fs.ensureDirSync(name); // Fs extra is used to extend node fs
  // Check whether the new folder is safe
  if(! isSafeToCreateProjectIn(root, name)) { process.exit(1);
  }

  // Write the package.json file to the new folder
  const packageJson = {
    name: appName,
    version: '0.1.0 from'.private: true}; fs.writeFileSync( path.join(root,'package.json'),
    JSON.stringify(packageJson, null.2));const useYarn = useNpm ? false : shouldUseYarn();
  const originalDirectory = process.cwd();
  process.chdir(root);
  // If NPM is used, check whether NPM is executed in the correct directory
  if(! useYarn && ! checkThatNpmCanReadCwd()) { process.exit(1);
  }

  // Determine the Node environment, print some prompts, and use the old version of react-scripts
  if(! semver.satisfies(process.version,'> = 6.0.0')) {
    // Output some prompt update information
    version = '[email protected]';
  }

  if(! useYarn) {// Check the NPM version. If the NPM version is lower than 3.x, use the old version of React-scripts
    const npmInfo = checkNpmVersion();
    if(! npmInfo.hasMinNpm) { version ='[email protected]'; }}// After judging, run the run method
  // Pass in the project path, project name, reactScripts version, whether to enter additional information, the path to run, templates (for development and debugging), and whether to use YARN
  run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
Copy the code

Createreactapp.js createApp portal I’ve simplified a few things here, deleted some output, and added some comments. The main thing createApp does is make some security judgments like: Check that the project name is valid, that the new version is safe, that the NPM version is compatible with the react-script version. The execution logic is written in the comment. After a series of checks, call the run method, passing in the project path, the project name, and the reactScripts version. Whether to enter additional information, the path to run, the template (for development and debugging), whether to use YARN. Once you understand the general flow, look at it function by function.

CheckAppName () // Check the validity of the incoming project name isSafeToCreateProjectIn(root, ShouldUseYarn () // Check yarn checkThatNpmCanReadCwd() // Check NPM run() // After the check, call run to perform installation and other operations

checkAppNameCheck that projectName is valid

function checkAppName(appName) {
  const validationResult = validateProjectName(appName);
  if(! validationResult.validForNewPackages) {// Check whether NPM specifications are met. If not, output a message and end the task
  }

  const dependencies = ['react'.'react-dom'.'react-scripts'].sort();
  if (dependencies.indexOf(appName) >= 0) {
    // Check whether the name is the same. If the name is the same, output a prompt and end the task}}Copy the code

CheckAppName is used to check whether the current project name complies with the NPM specification. For example, it cannot be capitated. The NPM package name is validate-npm-package-name. This simplifies most of the error code, but does not affect taste.

shouldUseYarnCheck whether yarn is installedcheckThatNpmCanReadCwdUsed to determine NPM

function shouldUseYarn() {
  try {
    execSync('yarnpkg --version', { stdio: 'ignore' });
    return true;
  } catch (e) {
    return false; }}Copy the code

run

This is where the real core installation logic comes in. __ starts installing dependencies, copying templates, etc.

function run(.) {
  // Here is the package to install, default is' react-scripts'. It may also be based on the pass to get the corresponding package
  const packageToInstall = getInstallPackage(version, originalDirectory);
  // Install all dependencies like react, react-dom, react-script
  const allDependencies = ['react'.'react-dom', packageToInstall]; . }Copy the code

Run gets a package to install based on the version passed in and the originalDirectory, originalDirectory. The default version is empty, the packageToInstall value is react-scripts, and then the packageToInstall value is concatenated to allDependencies, meaning allDependencies that need to be installed. React-scripts is a set of Webpack configurations and templates that are part of the other core of create-React-App. portal

function run(.) {.../ / get the package name, support taz | tar format, git repository, version number, the file path and so on
  getPackageName(packageToInstall)
    .then(packageName= >
      // If yarn is used, determine whether it is in online mode (corresponding to offline mode), and return to the next then processing
      checkIfOnline(useYarn).then(isOnline= > ({
        isOnline: isOnline,
        packageName: packageName,
      }))
    )
    .then(info= > {
      const isOnline = info.isOnline;
      const packageName = info.packageName;
      /** Start the installation part of the core by passing in 'install path', 'use YARN', 'all dependencies',' Output additional information ', 'online status' **/
      / * * this is the main operating according to the incoming parameters, began to run NPM | | yarn to install the react the react - dom rely on * * /
      /** if the network is not good, it may hang **/
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
        (a)= >packageName ); })... }Copy the code

If the yarn installation mode is used, check whether the yarn installation mode is offline. Add the packageToInstall and allDependencies dependencies to the install method.

Run method getInstallPackage (); // The default is react-scripts install(); // Install allDependencies init(); // call react-scripts/script/init to copy the template. Catch (); // Error handling

install

function install(root, useYarn, dependencies, verbose, isOnline) {
  // Use node to run installation scripts such as' NPM install react react-dom --save 'or' yarn add react react-dom
  return new Promise((resolve, reject) = > {
    let command;
    let args;

    // Start assembling the YARN command line
    if (useYarn) {
      command = 'yarnpkg';
      args = ['add'.'--exact']; // Use the exact version mode
      // Check whether the status is offline and add a status
      if(! isOnline) { args.push('--offline');
      }
      [].push.apply(args, dependencies);
      // Set CWD to the directory we want to install
      args.push('--cwd');
      args.push(root);
      // Output some information if it is offline

    } else {

      // NPM installation mode, the same as YARN
      command = 'npm';
      args = [
        'install'.'--save'.'--save-exact'.'--loglevel'.'error',
      ].concat(dependencies);

    }

    // If verbose is passed, add this parameter to output additional information
    if (verbose) {
      args.push('--verbose');
    }

    // execute the command line cross-platform with cross-spawn
    const child = spawn(command, args, { stdio: 'inherit' });

    // Close the handler
    child.on('close', code => {
      if(code ! = =0) {
        return reject({ command: `${command} ${args.join(' ')}`}); } resolve(); }); }); }Copy the code

As we follow the breakpoint from run to install, we can see that the code is split into two processing methods depending on whether yarn is used. If (useYarn) {yarn install logic} else {NPM install logic} {react,react-dom,react-script} Verbose and isOnline plus some command line arguments. Platform differences are handled with cross-spawn and will not be described here. See the above code for specific logic, remove unimportant information output, the code is relatively easy to understand.

Install Determine whether to use YARN or NPM based on the input parameters. Run the cross-spawn command to install dependencies required by NPM

After install returns a Promise, the breakpoint goes back to our run function to continue with the following logic.

function run() {... getPackageName() .then((a)= > {
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
            (a)= >packageName ); })... }Copy the code

Now that we have installed the dependencies we need for development, we can determine if the node we are currently running matches the version of node we have installed for the act-scripts packages.json. If the node version currently running is react-scripts, the dependency is required.

Then change the dependencies (react, react-dom, react-scripts) we have installed from the exact version eg(16.0.0) to the higher or equal version eg(^16.0.0). After that, our directory looks like this, with nothing in it but installed dependencies and package.json. So the next step is to generate some webpack configuration and a simple bootable demo.

So how does he make these things so fast? Remember that there is a hidden command line parameter, internal-tet-template, for developers to debug, so create-react-app generates this by simply copying the template from a path to the appropriate location. Isn’t that simple? HHHHH

run(...) {... getPackageName(packageToInstall) .then(...) .then(info= >install(...) .then((a)= > packageName))
    /** install Logic after installation **/
    /** Copy template logic from here **/
    .then(packageName= > {
      // After installing react, react-dom, and react-scripts, check whether the node version running in the current environment meets the requirements
      checkNodeVersion(packageName);
      // React, react-dom version range in package.json, eg: 16.0.0 => ^16.0.0
      setCaretRangeForRuntimeDeps(packageName);

      // Load the script and execute the init method
      const scriptsPath = path.resolve(
        process.cwd(),
        'node_modules',
        packageName,
        'scripts'.'init.js'
      );
      const init = require(scriptsPath);
      The init method mainly performs the following operations
      // Write some scripts to package.json. eg: script: {start: 'react-scripts start'}
      / / rewrite the README. MD
      // Copy the preset template to the project
      // Displays information about success and subsequent operations
      init(root, appName, verbose, originalDirectory, template);

      if (version === '[email protected]') {
        // If it is an older version of react-scripts output prompt
      }
    })
    .catch(reason= > {
      // Delete all installed files and output some log messages
    });
}
Copy the code

After the dependency is installed, run the checkNodeVersion command to check whether the Node version matches the dependency. Then concatenate the path to /node_modules/react-scripts/scripts/init.js and pass it to do some initialization. Then do something about the error

/node_modules/react-scripts/script/init.js

Target folder /node_modules/react-scripts/script/init.js

module.exports = function(appPath, appName, verbose, originalDirectory, template) {
  const ownPackageName = require(path.join(__dirname, '.. '.'package.json'))
    .name;
  const ownPath = path.join(appPath, 'node_modules', ownPackageName);
  const appPackage = require(path.join(appPath, 'package.json'));
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));

  // 1. Write the startup script to the destination package.json
  appPackage.scripts = {
    start: 'react-scripts start'.build: 'react-scripts build'.test: 'react-scripts test --env=jsdom'.eject: 'react-scripts eject'}; fs.writeFileSync( path.join(appPath,'package.json'),
    JSON.stringify(appPackage, null.2));// 2. Rewrite readme. MD to include some help information
  const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
  if (readmeExists) {
    fs.renameSync(
      path.join(appPath, 'README.md'),
      path.join(appPath, 'README.old.md')); }Public, SRC /[app.css, app.js, index.js,....] , .gitignore
  const templatePath = template
    ? path.resolve(originalDirectory, template)
    : path.join(ownPath, 'template');
  if (fs.existsSync(templatePath)) {
    fs.copySync(templatePath, appPath);
  } else {
    return;
  }
  fs.move(
    path.join(appPath, 'gitignore'),
    path.join(appPath, '.gitignore'),
    [],
    err => { /* Error handling */});// Install react and react-dom again
  let command;
  let args;
  if (useYarn) {
    command = 'yarnpkg';
    args = ['add'];
  } else {
    command = 'npm';
    args = ['install'.'--save', verbose && '--verbose'].filter(e= > e);
  }
  args.push('react'.'react-dom');

  const templateDependenciesPath = path.join(
    appPath,
    '.template.dependencies.json'
  );
  if (fs.existsSync(templateDependenciesPath)) {
    const templateDependencies = require(templateDependenciesPath).dependencies;
    args = args.concat(
      Object.keys(templateDependencies).map(key= > {
        return `${key}@${templateDependencies[key]}`; })); fs.unlinkSync(templateDependenciesPath); }if(! isReactInstalled(appPackage) || template) {const proc = spawn.sync(command, args, { stdio: 'inherit' });
    if(proc.status ! = =0) {
      console.error(` \ `${command} ${args.join(' ')}\` failed`);
      return; }}// 5. Logs are generated successfully
};
Copy the code

Init file is also a big head, processing logic mainly has

  1. Modify package.json by writing some startup scripts such asscript: {start: 'react-scripts start'}To start a development project
  2. Rewrite readme. MD to include some help information
  3. Copy the preset template to the projectpublic.src/[APP.css, APP.js, index.js,....]..gitignore
  4. Do some compatibility with older versions of Node. When selecting React-scripts, you can select the older @0.9.x version based on node versions.
  5. If the output is complete. If the output fails, perform operations such as outputting logs.

There’s a lot of code here, so I cut out a little bit, but if you’re interested in the original code you can jump here to see the react-scripts/scripts/init.js portal

END~

Now that the create-React-app project is part of the process, let’s review it:

  1. If the node version is smaller than 4, exit; otherwise, executecreateReactApp.jsfile
  2. createReactApp.jsDo some command line processing and response processing, and then determine if there’s any incomingprojectNameIf not, prompt and exit
  3. According to incomingprojectNameCreate a directory and createpackage.json.
  4. Determine whether there is a special requirement to install a versionreact-scriptsAnd then usecross-spawnTo handle cross-platform command line issues, useyarnornpmThe installationreact.react-dom.react-scripts.
  5. Run after installationreact-scripts/script/init.jsModify thepackage.jsonRun the script and copy the corresponding template to the directory.
  6. After processing this, output a prompt to the user.

I wanted to cover create-react-app, but I realized that there was only one creation, so IF I want to continue with React-scripts, I will write another one. React-scripts is probably a webpack configuration, so breakpoints don’t help much.