I posted this on The SegmentFault on January 23 of this year. The create-react-app source code for create-React-app is the source code for create-react-app. The create-react-app source code is the source code for create-React-app. I really want to texture, changed two words even if the original (although it seems that the ethos is such), so it is, I can say something!

The original article moved to the new blog address, reproduced to indicate the author’s source thank you!! If you think a change in a word or two is yours, just fucking forget it.

preface

This period of time to make things less, had a lot of time, as a new graduate entry newbie programmer, all dare not to relax, holds on for everyone thought I also want to do a little contribution for the open source community, but has no clear goals, recently in an effort to prepare dig through the react, In addition, the React scaffolding tool create-React-app is quite mature. When initializing a React project, it’s impossible to see how it built the development environment for me and how it did it. I still want to know, so I dragged it out.

If there are mistakes or need to correct the place, more advice, common progress.

Directions for use

As I said at the beginning, learning something new is about learning how to use it first and then seeing how it works. Create-react-app is an official scaffolding tool for quickly building a new React project, similar to vue’s vue-cli and Angular’s Angular-cli. As for why it’s not called react-CLI is a question worth pondering… Ha ha ha, interesting!

Without further discussion, post a picture of create-react-app command help.

outline

is required Only is required Only is required Only the project name is required, which means that when you use create-react-app, you must use the project name after the create-react-app.

  • create-react-app -V(or --version): This option can be used separately, print version information, each tool has basically?
  • create-react-app --info: This option can also be used alone to print the current system informationreactThe relevant development environment parameters, that is, what is the operating system,NodeVersion ah and so on, you can try it yourself.
  • create-react-app -h(or --help): This must be able to be used alone, otherwise how to print the help information, otherwise there would be no screenshots above.

That is to say, in addition to the above three parameters option can be out of the project name must be parameters to foreign used alone, because they had nothing to do with you to initialize the react project, and then the rest of the parameters is configured to react to initialize project, that is to say the three parameter can be appear at the same time, take a look at them respectively:

  • create-react-app <my-project> --verbose: Look at the picture above, print the local log, actually he isnpmandyarnInstall external dependencies can be added to the option, can print the installation error information.
  • create-react-app <my-project> --scripts-version: because it separates the create directory initialization step from the control command, it is used to controlreactThe development, packaging, and testing of the project are placed therereact-scriptsInside, so you can configure the control options separately, in case you don’t understand, but I’ll go into details.
  • create-react-app <my-project> --use-npmThe answer to this question is:create-react-appUse the defaultyarnTo install, run, if you are not usingyarn, you may need this configuration, specify usenpm.

Customization options

I would like to say a little more about — script-version. In the screenshots above, we can see that create-react-app already has four options. I did not try them all, because they are quite complicated. Let me begin with a rough guess of what they mean:

  • Specify version 0.8.2
  • innpmPublish your ownreact-scripts
  • Set one up on your website.tgzThe download package
  • Set one up on your website.tar.gzThe download package

Create-react-app is developer-friendly and allows you to define a lot of things on your own. If you don’t want to do that, it also provides standard React-Scripts for developers to use. I’ve always wondered about this. I’ll talk about the official React configuration separately later.

Directory analysis

The source code will certainly change as the version is iterated, but I’m here to download v1.1.0. You can download this version on Github.

The main description

Let’s take a look at its directory structure

├ ─ ─. Making ├ ─ ─ packages ├ ─ ─ the tasks ├ ─ ─ the eslintignore ├ ─ ─ the eslintrc ├ ─ ─ the gitignore ├ ─ ─. Travis. Yml ├ ─ ─ the yarnrc ├ ─ ─ Appveyor.cleanup -cache. TXT ├─ appveyor.yml ├─ CHANGELOG-0. X.m d ├─ CHANGELOG CONTRIBUTING. Md ├── LICENSE ├─ Package.├ ─ download.md ├─ Screencast.svgCopy the code

At first glance, how should I look at it? In fact, when I look at it carefully, it seems that many files can be seen at a glance. I can roughly say what each file is for, but I don’t know the specific ones.

  • .github: This is what you put in when you mention in this projectissueandprNorms of time
  • packagesThe package….. For the time being, I’ll tell you more about —->Focus on
  • tasks: Mission….. For the time being, I’ll tell you more about —->Focus on
  • .eslintignore: eslintIgnore files while checking
  • .eslintrc:eslintChecking the Configuration File
  • .gitignore:gitIgnore files when committing
  • .travis.yml:travisThe configuration file
  • .yarnrc:yarnThe configuration file
  • appveyor.cleanup-cache.txt: There’s a line in itEdit this file to trigger a cache rebuildEdit this file to trigger cache, specific why, temporarily not discussed
  • appveyor.yml:appveyorThe configuration file
  • CHANGELOG-0.x.md: Change description file starting with version 0.x
  • CHANGELOG.md: Current version change description file
  • CODE_OF_CONDUCT.md:facebookCode code specification
  • CONTRIBUTING.md: Core description of the project
  • lerna.json:lernaThe configuration file
  • LICENSE: Open source protocol
  • package.json: Project configuration file
  • README.md: Project instructions
  • screencast.svg: picture…

Are you getting cold feet after reading so many files? Json: packages, Tasks, package.json, packages, tasks, package.json I just want to tell you what these files are for, they all have their own functions, if you don’t know, refer to the reference links below.

Refer to the link

Eslint related: esLint website Travis related: Travis website Travis Getting started YARN related: Yarn website appveyor Related: Appveyor website Lerna related: Lerna website

Json, packages, package.json.

Looking for the entrance

Most of today’s front-end projects have many other dependencies. Unlike the old native javascript libraries, underscore, and so on, requires a single or two file that contains all of their contents. Although some frameworks do have umD specification files that can be read directly, Like Better scroll and so on, but in fact it was broken into many pieces when writing the source code, and finally integrated with the packaging tool. But scaffolding tools like create-React-app don’t seem to have the same way of looking at it. You have to find the entry point of the application, so the initial tool is definitely looking for the entry point.

Begin to pay close attention to

Which file should we start with when we get a project? I recommend starting with package.json files as long as they are managed based on NPM.

In theory, it should have a name, version, etc., but it doesn’t work. Look at a few important configurations.

"workspaces": [
    "packages/*"
],
Copy the code

As for workspaces, I did not find them in the NPM documentation, although we can guess it literally means that workspaces are packages in the actual working directory. Later I checked and found that workspaces are for local testing. The real useful files are in packages.

Focus on the

What we really need to focus on now are packages.

├ ─ ─ the Babel - preset - react - app - > no attention ├ ─ ─ the create - react - app ├ ─ ─ eslint - config - react - app - > no attention ├ ─ ─ the react - dev - utils - > ├─ react-error-overlay └─ react-scripts ├─ react-error ├─ ├─ react-scriptsCopy the code

There are six folders, wow, six separate projects, depending on the year and month….. After installing create-react-app, we typed the create-react-app command on the command line, so we boldly assumed that the create-react-app command would exist under create-react-app. The package.json file also exists in this directory. Now let’s split the 6 files into 6 projects for analysis.

"bin": {
    "create-react-app": "./index.js"
}
Copy the code

The bin in package.json file is a command that can be run on the command line. In other words, when executing the create-react-app command, we will execute the index.js file in the create-react-app directory.

Say more

The bin option in package.json is actually run based on the Node environment. For a simple example, executing create-react-app after installing create-react-app is equivalent to executing Node index.js.

Create-react-app directory parsing

After more than a series of search, we finally find the create difficult – the react – app command center entrance, regardless of other, we open the packages/create – react – app directory, a look at carefully, oh hey, only four files, four files we still persist? Besides package.json and readme. md, there are only two files left to view. Let’s take a look at these two files.

index.js

Json file to execute the index.js file. We’ll start with the index.js file and see what the source code looks like.

Except for a long string of comments, the code is very small, and it’s all posted:

var chalk = require('chalk');

var currentNodeVersion = process.versions.node; // Returns Node version information, or multiple versions if there are multiple versions
var semver = currentNodeVersion.split('. '); // A collection of all Node versions
var major = semver[0]; // Retrieve the first Node version information

// If the current version is less than 4 print the following information and terminate the process
if (major < 4) {
  console.error(
    chalk.red(
      'You are running Node ' +
        currentNodeVersion +
        '.\n' +
        'Create React App requires Node 4 or higher. \n' +
        'Please update your version of Node.')); process.exit(1); // Terminate the process
}

// If there is no less than 4, import the following file and continue executing
require('./createReactApp');
Copy the code

Zha look past actually you know it is probably what meaning…. If the node.js version is smaller than 4, it will not be executed. Let’s take a look at it separately. Here, he uses a library chalk, which is not complicated to understand, and parses line by line.

  • chalkThe practical effect of this on this code is to discolor the output information on the command line. This is where the library comes in to change the style of the output on the command line.NPM address

There are several Node apis:

  • process.versionsReturns an object containingNodeAnd its dependency information
  • process.exitThe end of theNodeThe process,1Yes Status code, indicating that exceptions are not processed

After we pass through index.js, we come to createreactApp.js. Let’s continue.

createReactApp.js

When the Node version of our machine is larger than 4, we will continue to execute this file, open this file, there is a lot of code, about 700 lines, we slowly disassemble.

Here to put a little skill, reading the source code, you can write code in open a window, follow to write it again, out of the code can first remove in the source file, 700 lines of code, so when you read the 200 line, the source file is only 500, not only with a sense of accomplishment to continue reading, also does not perform logic removed first, not affect you read somewhere else.

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');

const packageJson = require('./package.json');
Copy the code

Open code a row of dependencies, mengfu…. I can’t look up dependencies one by one, can I? So, my advice is to leave it alone and come back to what it does when you use it, to understand it a little bit better, and keep going.

let projectName; // defines a variable to store the project name

const program = new commander.Command(packageJson.name)
  .version(packageJson.version) // Enter the version information. 'create-react-app -v' is used to print the version information
  .arguments('<project-directory>') // Use 'create-react-app 
      
       ' arguments in Angle brackets
      
  .usage(`${chalk.green('<project-directory>')} [options]`) // Use the information printed on the first line of 'create-react-app', which is the instructions
  .action(name= > {
    projectName = name; // The argument to the action function is 
      
  })
  .option('--verbose'.'print additional logs') // option configures the 'create-react-app -[option]' option, similar to --help -v
  .option('--info'.'print environment debug info') // Print the local development environment, operating system, 'Node' version, etc
  .option(
    '--scripts-version <alternative-package>'.'use a non-standard version of react-scripts'
  ) // I said this before, specify special 'react-scripts'
  .option('--use-npm') // 'yarn' is used by default, NPM is specified
  .allowUnknownOption() 'create-react-app 
      
        -la' is not defined, but I can still do it without saving it
      
  .on('--help', () = > {// Some printed information is omitted here
  }) // on('--help') is used to customize printed help information. When 'create-react-app -h(or --help)' is used to execute the code, which is basically printed information
  .parse(process.argv); // This is to parse our normal 'Node' process. Commander can't take over 'Node' without this
Copy the code

In the above code, I have omitted the extraneous printout. This code is a key entry point to the file. What if we go back to its dependency and find that it is an external dependency? It’s not possible to go to node_modules and look for external dependencies. Simply go to NPM and look for external dependencies.

  • commander: To summarize,NodeCommand interface, that is, you can use it to administerNodeCommand.NPM address

The above is just an implementation of the use of Commander. There is nothing to be said for this. The definition of Commander is the stuff we see on the command line, such as parameters, such as print messages, etc. Let’s move on.

// Check if 'create-react-app 
      
       ' is executed on the command line. If not, continue
      
if (typeof projectName === 'undefined') {
  // If the --info option is used to execute the following code when no name is passed, no error will be reported if --info is configured
  if (program.info) {
    // Prints the current environment and packages' react ', 'react-dom', and 'react-scripts'
    envinfo.print({
      packages: ['react'.'react-dom'.'react-scripts'].noNativeIDE: true.duplicates: true}); process.exit(0); // Exit the process normally
  }
  // A bunch of errors are printed without the project name and the --info option, like --version and --help are options of Commander, so you don't need to configure them separately
  console.error('Please specify the project directory:');
  console.log(
    `  ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`
  );
  console.log();
  console.log('For example:');
  console.log(`  ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`);
  console.log();
  console.log(
    `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
  );
  process.exit(1); // Throw an exception to exit the process
}
Copy the code

Remember that the project name in create-react-app

was assigned to the projectName variable above? The function here is to see if the user has passed the

parameter. If not, an error is reported and some help information is displayed. Another external dependency envinfo is used here.

  • envinfo: Displays information about the current operating system environment and the specified package.NPM address

Here I also want to tease the segmentFault editor… I open the view and edit the card at the same time… Over your face. PNG!

There’s one thing I left out, but I’ll show you:

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

Create-react-app generates a standard folder when initializing a project. There is a hidden option –internal-testing-template — to change the template used to initialize the directory. So this option is not recommended.

Moving on, there are several pre-defined functions, but let’s ignore them and find the first function to be executed:

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

A createAPP function that takes five arguments

  • projectName: performcreate-react-app <name>The value of name, which is the name of the initialization project
  • program.verbose: Here is to saycommandertheoptionChoice, if you add this choice this value is going to betrueOtherwise, it isfalseThat is to say if I add it here--verbose, so this parameter istrueAnd as for theverboseWhat is it, as I said before, inyarnornpmThe installation prints local information, which means we can find additional information if something goes wrong during the installation.
  • program.scriptsVersion: Same as above, specifyreact-scriptsversion
  • program.useNpm: In the same way, specify whether to use itnpmBy defaultyarn
  • hiddenProgram.internalTestingTemplateI have already added the template to specify initialization, they said internal use, we can ignore, should be used to develop test template directory for use.

Now that we’ve found the first function to execute createApp, let’s see what createApp does.

createApp()

function createApp(name, verbose, version, useNpm, template) {
  const root = path.resolve(name); // Get the location where the current process is running, i.e. the absolute path to the file directory
  const appName = path.basename(root); // Return to the last part of the root path

  checkAppName(appName); // Run the checkAppName function to check whether the file name is valid
  fs.ensureDirSync(name); // The ensureDirSync method is the external dependency package fs-extra, not the fs module of Node itself, which ensures that the specified file name exists in the current directory
  // isSafeToCreateProjectIn Function Checks whether the folder is secure
  if(! isSafeToCreateProjectIn(root, name)) { process.exit(1); // The process was terminated illegally
  }
  // Print here successfully created a 'react' project in the specified directory
  console.log(`Creating a new React app in ${chalk.green(root)}. `);
  console.log();
  // Define the package.json base
  const packageJson = {
    name: appName,
    version: '0.1.0 from'.private: true};// Write the package.json file to the folder we created
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null.2));// Define constant useYarn if the pass parameter has --use-npm useYarn is false, otherwise shouldUseYarn() is executed to check whether YARN exists
  If NPM is specified, useYarn is false. If NPM is specified, shouldUseYarn is used
  // shouldUseYarn is used to check whether 'YARN' is installed on the machine
  const useYarn = useNpm ? false : shouldUseYarn();
  // Get the directory of the current Node process. The next line of code will change this value, so if I use this value later, it will not actually get this value later
  // This is the directory where I performed the initialization project, not the initialized directory, which is the parent directory of the initialization.
  const originalDirectory = process.cwd();
  // Change the process directory to the bottom subprocess directory
  // Change the process directory here to the directory we created
  process.chdir(root);
  // If no yarn is used and the checkThatNpmCanReadCwd() function is not correct
  // checkThatNpmCanReadCwd this function is used to check whether the process directory is the directory we created. If the process is not in the directory we created, it will cause errors during the subsequent 'NPM' installation
  if(! useYarn && ! checkThatNpmCanReadCwd()) { process.exit(1);
  }
  // Compare node versions and warn if they are less than 6
  < 6, 'react-scripts' standard version is 0.9. X, that is, the standard' [email protected] 'and above does not support' node 'under 6
  if(! semver.satisfies(process.version,'> = 6.0.0')) {
    console.log(
      chalk.yellow(
        `You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
          `Please update to Node 6 or higher for a better, fully supported experience.\n`));// Fall back to latest supported react-scripts on Node 4
    version = '[email protected]';
  }
  // Warn if yarn is not used
  // If 'NPM' is at least 3, install '[email protected]'
  if(! useYarn) {const npmInfo = checkNpmVersion();
    if(! npmInfo.hasMinNpm) {if (npmInfo.npmVersion) {
        console.log(
          chalk.yellow(
            `You are using npm ${npmInfo.npmVersion} so the project will be boostrapped with an old unsupported version of tools.\n\n` +
              `Please update to npm 3 or higher for a better, fully supported experience.\n`)); }// Fall back to latest supported react-scripts for npm 3
      version = '[email protected]'; }}// Execute the run function with these parameters
  // After executing the above code, the 'run' function will be executed, but I will first say all the functions used above, before the next core function 'run'
  run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
Copy the code

I first come here to summarize this function all did what things, take a look at what he used to rely on, say what do the first, in our directory to create a project directory, and check the directory name is legal, if this directory is safe, and then to the one in the package. The json file, It determines which version of React-scripts should be used in the current environment, and then executes the run function. Let’s see what external dependencies this function uses:

  • fs-extra: External dependencies,NodeExternal extension module for the built-in file moduleNPM address
  • semver: External dependencies for comparisonNodeversionNPM address

After that, I will analyze the function dependency in detail, except for a small number of very simple functions, and then let’s look at the function dependency of this function:

  • checkAppName(): used to check whether the file name is valid.
  • isSafeToCreateProjectIn(): Checks whether the folder is secure
  • shouldUseYarn(): Used for testingyarnWhether it has been installed on the machine
  • checkThatNpmCanReadCwd(): Used for testingnpmWhether to execute in the correct directory
  • checkNpmVersion(): Used for testingnpmWhether it has been installed on the machine

checkAppName()

function checkAppName(appName) {
  // Check that the package name is valid with validateProjectName. This validateProjectName is a reference to external dependencies, as described below
  const validationResult = validateProjectName(appName); 
  // If there is an error continuation in the object, this is the specific use of external dependencies
  if(! validationResult.validForNewPackages) {console.error(
      `Could not create a project called ${chalk.red(
        `"${appName}"`
      )} because of npm naming restrictions:`
    );
    printValidationResults(validationResult.errors);
    printValidationResults(validationResult.warnings);
    process.exit(1);
  }
  
  // Defines three development dependency names
  const dependencies = ['react'.'react-dom'.'react-scripts'].sort();
  // If the project uses all three names, an error will be reported and the process will exit
  if (dependencies.indexOf(appName) >= 0) {
    console.error(
      chalk.red(
        `We cannot create a project called ${chalk.green( appName )} because a dependency with the same name exists.\n` +
          `Due to the way npm works, the following names are not allowed:\n\n`
      ) +
        chalk.cyan(dependencies.map(depName= > `  ${depName}`).join('\n')) +
        chalk.red('\n\nPlease choose a different project name.')); process.exit(1); }}Copy the code

This function it is actually quite simple, use an external dependencies to check the file name is in accordance with the specification of NPM package file names, and then defines three can’t name the react and react – dom, the react – scripts, external dependencies:

  • validate-npm-package-name: External dependencies, check whether the package name is valid.NPM address

Where the function depends on:

  • printValidationResults(): function reference, this function is what I call a very simple type, inside the received error message to print a loop, nothing to say.

isSafeToCreateProjectIn()

function isSafeToCreateProjectIn(root, name) {
  // Define a bunch of file names
  // I took a closer look at some this morning, and the following files are the ones we developers mentioned in 'create-react-app'
  const validFiles = [
    '.DS_Store'.'Thumbs.db'.'.git'.'.gitignore'.'.idea'.'README.md'.'LICENSE'.'web.iml'.'.hg'.'.hgignore'.'.hgcheck'.'.npmignore'.'mkdocs.yml'.'docs'.'.travis.yml'.'.gitlab-ci.yml'.'.gitattributes',];console.log();

  // If there are no other files in the project folder we created, it will return true
  const conflicts = fs
    .readdirSync(root)
    .filter(file= >! validFiles.includes(file));if (conflicts.length < 1) {
    return true;
  }
  // Otherwise the folder is not secure, and there are any insecure files printed next to it
  console.log(
    `The directory ${chalk.green(name)} contains files that could conflict:`
  );
  console.log();
  for (const file of conflicts) {
    console.log(`  ${file}`);
  }
  console.log();
  console.log(
    'Either try using a new directory name, or remove the files listed above.'
  );
  // And return false
  return false;
}
Copy the code

The create-react-app function checks whether the directory created contains any files other than those listed in the above validFiles directory. This is how create-react-app was developed.

shouldUseYarn()

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

Three lines of… ExecSync is referenced by node’s own child_process module, which is used to execute commands. This function is to execute yarnpkg –version to determine whether yarn has been properly installed. If yarn has not been properly installed, UseYarn is still false, even if –use-npm is not specified.

  • execSync: since the referencechild_process.execSyncIs used to execute the child process that needs to be executed

checkThatNpmCanReadCwd()

function checkThatNpmCanReadCwd() {
  const cwd = process.cwd(); // Get the current process directory
  let childOutput = null; // Define a variable to hold information about 'NPM'
  try {
    // this is equivalent to executing 'NPM config list' and combining its output into a string
    childOutput = spawn.sync('npm'['config'.'list']).output.join(' ');
  } catch (err) {
    return true;
  }
  // Check if it is a string
  if (typeofchildOutput ! = ='string') {
    return true;
  }
  // Separate the entire string with a newline character
  const lines = childOutput.split('\n');
  // Define a prefix for the information we need
  const prefix = '; cwd = ';
  // Go to every line in the entire lines to find a line with this prefix
  const line = lines.find(line= > line.indexOf(prefix) === 0);
  if (typeofline ! = ='string') {
    return true;
  }
  // The directory where 'NPM' is executed
  const npmCWD = line.substring(prefix.length);
  // Check whether the current directory is the same as the execution directory
  if (npmCWD === cwd) {
    return true;
  }
  // The NPM process is not running in the correct directory
  console.error(
    chalk.red(
      `Could not start an npm process in the right directory.\n\n` +
        `The current directory is: ${chalk.bold(cwd)}\n` +
        `However, a newly started npm process runs in: ${chalk.bold( npmCWD )}\n\n` +
        `This is probably caused by a misconfigured system terminal shell.`));// Here he makes some separate judgments about the Windows situation, without delving into the information
  if (process.platform === 'win32') {
    console.error(
      chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
        `  ${chalk.cyan(
          'reg'
        )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
        `  ${chalk.cyan(
          'reg'
        )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
        chalk.red(`Try to run the above two lines in the terminal.\n`) +
        chalk.red(
          `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`)); }return false;
}
Copy the code

I am really sorry that I pasted this function wrong before. The code above has been parsed, and an external dependency has been used:

  • cross-spawnDid I mention that before? Forget, used to executenodeProcess.NPM address

Why use an external dependency instead of Node’s own? Take a look at cross-spawn itself, a Node cross-platform solution that solves various problems on Windows.

checkNpmVersion()

function checkNpmVersion() {
  let hasMinNpm = false;
  let npmVersion = null;
  try {
    npmVersion = execSync('npm --version')
      .toString()
      .trim();
    hasMinNpm = semver.gte(npmVersion, '3.0.0');
  } catch (err) {
    // ignore
  }
  return {
    hasMinNpm: hasMinNpm,
    npmVersion: npmVersion,
  };
}
Copy the code

Return an object with two pairs, one is the NPM version number, one is whether there is a minimum NPM version limit, one is an external dependency, and one is a Node API, which I have mentioned before.

CreateApp () creates a dependency and executes a function called run().

The run() function is executed after the createApp() function has finished executing, and it takes seven arguments.

  • root: The absolute path to the directory we created
  • appName: The name of the directory we created
  • version;react-scriptsThe version of the
  • verbose: continue passingverboseIn thecreateAppIs not used in
  • originalDirectory: Original directory, this was mentioned earlier, hererunFunction is useful
  • tempalte: template, this parameter has been mentioned before, is not used externally
  • useYarn: Whether to useyarn

Specifically, look at the run() function below.

run()

function run(root, appName, version, verbose, originalDirectory, template, useYarn) {
  // There is a lot of processing done to 'react-scripts'
  const packageToInstall = getInstallPackage(version, originalDirectory); // Get dependency package information
  const allDependencies = ['react'.'react-dom', packageToInstall]; // All development dependency packages

  console.log('Installing packages. This might take a couple of minutes.');
  getPackageName(packageToInstall) // Get the original name of the dependency package and return
    .then(packageName= >
      // Check if the mode is offline and return the result and package name
      checkIfOnline(useYarn).then(isOnline= > ({
        isOnline: isOnline,
        packageName: packageName,
      }))
    )
    .then(info= > {
      // The package name described above is received and whether it is in offline mode
      const isOnline = info.isOnline;
      const packageName = info.packageName;
      console.log(
        `Installing ${chalk.cyan('react')}.${chalk.cyan(
          'react-dom'
        )}, and ${chalk.cyan(packageName)}. `
      );
      console.log();
      // Install dependencies
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
        (a)= > packageName
      );
    })
    .then(packageName= > {
      // Check whether the current Node version supports packages
      checkNodeVersion(packageName);
      // Check whether development dependencies for 'package.json' are normal
      setCaretRangeForRuntimeDeps(packageName);
      // 'react-scripts' is the directory of the script
      const scriptsPath = path.resolve(
        process.cwd(),
        'node_modules',
        packageName,
        'scripts'.'init.js'
      );
      // Introduce the 'init' function
      const init = require(scriptsPath);
      // Perform a copy of the directory
      init(root, appName, verbose, originalDirectory, template);
      // Issue a warning when 'react-scripts' version is 0.9.x
      if (version === '[email protected]') {
        console.log(
          chalk.yellow(
            `\nNote: the project was boostrapped with an old unsupported version of tools.\n` +
              `Please update to Node >=6 and npm >=3 to get supported tools in new projects.\n`)); }})// Exception handling
    .catch(reason= > {
      console.log();
      console.log('Aborting installation.');
      // Determine specific errors based on commands
      if (reason.command) {
        console.log(`  ${chalk.cyan(reason.command)} has failed.`);
      } else {
        console.log(chalk.red('Unexpected error. Please report it as a bug:'));
        console.log(reason);
      }
      console.log();

      // If an exception occurs, the files in the directory will be deleted
      const knownGeneratedFiles = [
        'package.json'.'npm-debug.log'.'yarn-error.log'.'yarn-debug.log'.'node_modules',];// Next to delete
      const currentFiles = fs.readdirSync(path.join(root));
      currentFiles.forEach(file= > {
        knownGeneratedFiles.forEach(fileToMatch= > {
          if (
            (fileToMatch.match(/.log/g) && file.indexOf(fileToMatch) === 0) ||
            file === fileToMatch
          ) {
            console.log(`Deleting generated file... ${chalk.cyan(file)}`); fs.removeSync(path.join(root, file)); }}); });// Check whether files exist in the current directory
      const remainingFiles = fs.readdirSync(path.join(root));
      if(! remainingFiles.length) {console.log(
          `Deleting ${chalk.cyan(`${appName}/ `)} from ${chalk.cyan(
            path.resolve(root, '.. '))}`
        );
        process.chdir(path.resolve(root, '.. '));
        fs.removeSync(path.join(root));
      }
      console.log('Done.');
      process.exit(1);
    });
}
Copy the code

Create-react-app init is used to initialize a project with create-react-app init . You can specify a version of a React-script or whatever the external defines itself.

His references to the run() function are all made in the form of Promise callbacks. Since I have been in the habit of async/await Node, I am not familiar with promises. Let’s take a look at the list of functions that use external dependencies.

  • getInstallPackage(): Gets the one to installreact-scriptsVersion or developer defined versionreact-scripts
  • getPackageName(): Get the formalreact-scriptsThe package name
  • checkIfOnline(): Check whether the network connection is normal
  • install(): Installs the development dependency packages
  • checkNodeVersion()Check:NodeVersion information
  • setCaretRangeForRuntimeDeps(): Check whether the development dependency is installed correctly and the version is correct
  • init(): Copy the predefined directory files into my project

With a general idea, let’s analyze each function one by one:

getInstallPackage()

function getInstallPackage(version, originalDirectory) {
  let packageToInstall = 'react-scripts'; // Define the constant packageToInstall, which defaults to the standard 'react-scripts' package name
  const validSemver = semver.valid(version); // Verify that the version number is valid
  if (validSemver) {
    packageToInstall += ` @${validSemver}`; // Install 'react-scripts' with @x.x.x. // install' react-scripts' with @x.x.x
  } else if (version && version.match(/^file:/)) {
    // Invalid and version number argument with 'file:' executes the following code to specify the installation package as our own defined package
    packageToInstall = `file:${path.resolve(
      originalDirectory,
      version.match(/^file:(.*)? $/) [1])}`;
  } else if (version) {
    // Invalid 'tar.gz' file that does not start with 'file:' and defaults to online
    // for tar.gz or alternative paths
    packageToInstall = version;
  }
  // Returns the final 'react-scripts' information to install, or the version number or local file or online'.tar.gz 'resource
  return packageToInstall;
}
Copy the code

This method takes two arguments, version and originalDirectory, which determine what react-scripts should install, depending on each line.

The create-react-app itself provides three mechanisms for installing react-Scripts. The initial project can specify the react-Scripts version or customize it, so it provides these mechanisms. There is only one external dependency, Semver, which was mentioned before, but not much more.

getPackageName()

function getPackageName(installPackage) {
  // The react-scripts function installs the package based on the above information, which returns the proper package name
  // Here is the case for the online 'tar.gz' package
  if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
    // Create a temporary directory for the online.tar.gz package
    return getTemporaryDirectory()
      .then(obj= > {
        let stream;
        if (/^http/.test(installPackage)) {
          stream = hyperquest(installPackage);
        } else {
          stream = fs.createReadStream(installPackage);
        }
        return extractStream(stream, obj.tmpdir).then((a)= > obj);
      })
      .then(obj= > {
        const packageName = require(path.join(obj.tmpdir, 'package.json')).name;
        obj.cleanup();
        return packageName;
      })
      .catch(err= > {
        console.log(
          `Could not extract the package name from the archive: ${err.message}`
        );
        const assumedProjectName = installPackage.match(
          / ^. + \ / (. +?) (? :-\d+.+)? \.(tgz|tar\.gz)$/) [1];
        console.log(
          `Based on the filename, assuming it is "${chalk.cyan( assumedProjectName )}"`
        );
        return Promise.resolve(assumedProjectName);
      });
  // this is the case where 'git+' information is included
  } else if (installPackage.indexOf('git+') = = =0) {
    return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)? $/) [1]);
  // Only version information is available
  } else if (installPackage.match(+ @ /. /)) {
    return Promise.resolve(
      installPackage.charAt(0) + installPackage.substr(1).split(The '@') [0]);// This is the case where the message begins with 'file:'
  } else if (installPackage.match(/^file:/)) {
    const installPackagePath = installPackage.match(/^file:(.*)? $/) [1];
    const installPackageJson = require(path.join(installPackagePath, 'package.json'));
    return Promise.resolve(installPackageJson.name);
  }
  // Return nothing directly to the package name
  return Promise.resolve(installPackage);
}
Copy the code

The purpose of this function is to return a normal dependency package name, such as react-scripts if we have nothing, or my-react-scripts if we have defined our own package. Take an installPackage argument and execute it as a Promise callback from the start to the end. Let’s take a look at what this function does by looking at the comments on each line above.

To sum up, this function returns the normal package name, without any symbols, to look at its external dependencies:

  • hyperquest: This is used to stream HTTP requests.NPM address

It also has function dependencies, and I won’t talk about either of them separately. The meaning of function is easy to understand, but I haven’t figured out why:

  • getTemporaryDirectory(): no, it is a callback function that creates a temporary directory.
  • extractStream(): Mainly usednodeI really don’t understand why the medicine is changed to the form of flow, so I don’t express my opinion. In fact, I still don’t understand. To really understand is to have a try, but it’s really a bit troublesome, I don’t want to pay attention to it.

PS: In fact, this function is easy to understand is to return the normal package name, but there are some processing I do not understand, later understanding deep backtracking.

checkIfOnline()

function checkIfOnline(useYarn) {
  if(! useYarn) {return Promise.resolve(true);
  }

  return new Promise(resolve= > {
    dns.lookup('registry.yarnpkg.com', err => {
      let proxy;
      if(err ! =null && (proxy = getProxy())) {
        dns.lookup(url.parse(proxy).hostname, proxyErr => {
          resolve(proxyErr == null);
        });
      } else {
        resolve(err == null); }}); }); }Copy the code

If NPM is used, it returns true. This function is available because yarn has a function called offline installation. This function determines whether to install yarn offline.

  • dns: checks whether a request can be made to the specified address.NPM address

install()

function install(root, useYarn, dependencies, verbose, isOnline) {
  // encapsulate in a callback function
  return new Promise((resolve, reject) = > {
    let command; // Define a command
    let args;  // Define the parameters of a command
    // If yarn is used
    if (useYarn) {
      command = 'yarnpkg';  // Command name
      args = ['add'.'--exact']; // The base of command arguments
      if(! isOnline) { args.push('--offline');  // Use the above function to check whether the mode is offline
      }
      [].push.apply(args, dependencies); // Combination parameters and development rely on 'react' 'react-dom' 'react-scripts'
      args.push('--cwd'); // Specify the address of the command execution directory
      args.push(root); // The absolute path to the address
      // A warning is issued when using offline mode
      if(! isOnline) {console.log(chalk.yellow('You appear to be offline.'));
        console.log(chalk.yellow('Falling back to the local Yarn cache.'));
        console.log();
      }
    // NPM is used without YARN
    } else {
      // This is the same as above, the command defines a combination of parameters
      command = 'npm';
      args = [
        'install'.'--save'.'--save-exact'.'--loglevel'.'error',
      ].concat(dependencies);
    }
    // Since both 'yarn' and 'NPM' can take this parameter, they are separated and splice to the top
    if (verbose) {
      args.push('--verbose');
    }
    // Execute the command as a group
    const child = spawn(command, args, { stdio: 'inherit' });
    // Close the command
    child.on('close', code => {
      // if code is 0, the command is closed normally
      if(code ! = =0) {
        reject({
          command: `${command} ${args.join(' ')}`});return;
      }
      // Proceed as normal
      resolve();
    });
  });
}
Copy the code

Again, the key point is to take a look at each line of code comments. The function here is to combine a YARN or NPM install command and install these modules into the project folder. The cross-spawn dependency used in this function is described earlier, but not mentioned.

At this point, create-react-app has created the package.json directory and installed all the dependencies, react, react-dom, and react-scrpts.

checkNodeVersion()

function checkNodeVersion(packageName) {
  // Find 'react-scripts' package.json' path
  const packageJsonPath = path.resolve(
    process.cwd(),
    'node_modules',
    packageName,
    'package.json'
  );
  // Import 'react-scripts' package.json'
  const packageJson = require(packageJsonPath);
  // In 'package.json' we define a 'engine' that contains the 'Node' version information. You can open the source code 'packages/react-scripts/package.json' to see more information
  if(! packageJson.engines || ! packageJson.engines.node) {return;
  }
  // Compare the Node version information of the process with the minimum supported version. If it is smaller than this, an error will be reported and the process will exit
  if(! semver.satisfies(process.version, packageJson.engines.node)) {console.error(
      chalk.red(
        'You are running Node %s.\n' +
          'Create React App requires Node %s or higher. \n' +
          'Please update your version of Node.'
      ),
      process.version,
      packageJson.engines.node
    );
    process.exit(1); }}Copy the code

Check the Node version. React-scrpts relies on Node versions, which are not supported by earlier versions of Node.

setCaretRangeForRuntimeDeps()

function setCaretRangeForRuntimeDeps(packageName) {
  const packagePath = path.join(process.cwd(), 'package.json');  // Retrieve the 'package.json' path in the directory where the project was created
  const packageJson = require(packagePath); / / introduce ` package. Json `
  // Check if 'dependencies' exists
  if (typeof packageJson.dependencies === 'undefined') {
    console.error(chalk.red('Missing dependencies in package.json'));
    process.exit(1);
  }
  // Take out 'react-scripts' or custom to see if' package.json 'exists
  const packageVersion = packageJson.dependencies[packageName];
  if (typeof packageVersion === 'undefined') {
    console.error(chalk.red(`Unable to find ${packageName} in package.json`));
    process.exit(1);
  }
  // Check 'react' and 'react-dom' versions
  makeCaretRange(packageJson.dependencies, 'react');
  makeCaretRange(packageJson.dependencies, 'react-dom');
  // Rewrite the file 'package.json'
  fs.writeFileSync(packagePath, JSON.stringify(packageJson, null.2));
}
Copy the code

This function, I don’t want to say too much about it, is used to check whether the dependencies we installed have been written to package.json, and to check the version of the dependencies.

  • makeCaretRange(): used to check dependent versions

I didn’t analyze the subfunctions separately because I didn’t think it was difficult and didn’t have much impact on the main line, so I didn’t want to go into too much detail.

Here createreactapp.js inside the source code are analyzed, yi! The init() function is stored in packages/react-scripts/script, but it is not related to the react-scripts package. Is a function that copies the template directory structure defined by itself.

init()

It itself receives five parameters:

  • appPathBefore:root, the absolute path of the project
  • appName: Project name
  • verboseI’ve said this before,npmAdditional information at installation time
  • originalDirectory: Original directory, the directory in which the command is executed
  • templateThere is only one type of template. This option configures the function I mentioned earlier, the test template
// The current package name, which is the package for this command
const ownPackageName = require(path.join(__dirname, '.. '.'package.json')).name;
// The path of the current package
const ownPath = path.join(appPath, 'node_modules', ownPackageName);
// Project's 'package.json'
const appPackage = require(path.join(appPath, 'package.json'));
// Check whether the project has' yarn.lock 'to determine whether to use' yarn '
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));

appPackage.dependencies = appPackage.dependencies || {};

// Define 'scripts'
appPackage.scripts = {
  start: 'react-scripts start'.build: 'react-scripts build'.test: 'react-scripts test --env=jsdom'.eject: 'react-scripts eject'};// Rewrite 'package.json'
fs.writeFileSync(
  path.join(appPath, 'package.json'),
  JSON.stringify(appPackage, null.2));// Check whether the project directory has readme. md, which is already defined in the template directory to prevent conflicts
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
  fs.renameSync(
    path.join(appPath, 'README.md'),
    path.join(appPath, 'README.old.md')); }// Whether there is a template option. The default is' template 'directory in the current command package directory, i.e.' packages/react-scripts/tempalte '
const templatePath = template
  ? path.resolve(originalDirectory, template)
  : path.join(ownPath, 'template');
if (fs.existsSync(templatePath)) {
  // Copy the directory to the project directory
  fs.copySync(templatePath, appPath);
} else {
  console.error(
    `Could not locate supplied template: ${chalk.green(templatePath)}`
  );
  return;
}
Copy the code

I’m not going to post all the code for this function, it’s pretty easy to understand what’s in it, it’s basically just changes to directory structures and rename those. Pick a few. So far, create-React-app from zero to directory dependencies, the installed source code has been analyzed, but it’s really just initializing directories and dependencies, The code that controls the environment is in React-Scripts, so it’s a bit far from where I’d like to be, but I’m not going to cover that now because this article is a long one.

I hope this article was helpful.

long-winded

In creation-react-app, webpack configuration, esLint configuration, Babel configuration….. Packages /create-react-app and Packages /react-script. Packages /create-react-app (create-react-script) (create-react-script) (create-react-script) (create-react-script) Write a separate article on the source code analysis of Packages/React-Script.

The code word is not easy, there may be a mistake of what, say not clear, say wrong, welcome to correct, forgive!