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
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 informationreact
The relevant development environment parameters, that is, what is the operating system,Node
Version 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 isnpm
andyarn
Install 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 controlreact
The development, packaging, and testing of the project are placed therereact-scripts
Inside, 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-npm
The answer to this question is:create-react-app
Use the defaultyarn
To 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
- in
npm
Publish your ownreact-scripts
- Set one up on your website
.tgz
The download package - Set one up on your website
.tar.gz
The 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 projectissue
andpr
Norms of timepackages
The package….. For the time being, I’ll tell you more about —->Focus ontasks
: Mission….. For the time being, I’ll tell you more about —->Focus on.eslintignore
:eslint
Ignore files while checking.eslintrc
:eslint
Checking the Configuration File.gitignore
:git
Ignore files when committing.travis.yml
:travis
The configuration file.yarnrc
:yarn
The configuration fileappveyor.cleanup-cache.txt
: There’s a line in itEdit this file to trigger a cache rebuild
Edit this file to trigger cache, specific why, temporarily not discussedappveyor.yml
:appveyor
The configuration fileCHANGELOG-0.x.md
: Change description file starting with version 0.xCHANGELOG.md
: Current version change description fileCODE_OF_CONDUCT.md
:facebook
Code code specificationCONTRIBUTING.md
: Core description of the projectlerna.json
:lerna
The configuration fileLICENSE
: Open source protocolpackage.json
: Project configuration fileREADME.md
: Project instructionsscreencast.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.
chalk
The 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.versions
Returns an object containingNode
And its dependency informationprocess.exit
The end of theNode
The process,1
Yes 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,Node
Command interface, that is, you can use it to administerNode
Command.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 projectprogram.verbose
: Here is to saycommander
theoption
Choice, if you add this choice this value is going to betrue
Otherwise, it isfalse
That is to say if I add it here--verbose
, so this parameter istrue
And as for theverbose
What is it, as I said before, inyarn
ornpm
The installation prints local information, which means we can find additional information if something goes wrong during the installation.program.scriptsVersion
: Same as above, specifyreact-scripts
versionprogram.useNpm
: In the same way, specify whether to use itnpm
By defaultyarn
hiddenProgram.internalTestingTemplate
I 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,Node
External extension module for the built-in file moduleNPM addresssemver
: External dependencies for comparisonNode
versionNPM 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 secureshouldUseYarn()
: Used for testingyarn
Whether it has been installed on the machinecheckThatNpmCanReadCwd()
: Used for testingnpm
Whether to execute in the correct directorycheckNpmVersion()
: Used for testingnpm
Whether 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.execSync
Is 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-spawn
Did I mention that before? Forget, used to executenode
Process.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 createdappName
: The name of the directory we createdversion
;react-scripts
The version of theverbose
: continue passingverbose
In thecreateApp
Is not used inoriginalDirectory
: Original directory, this was mentioned earlier, hererun
Function is usefultempalte
: template, this parameter has been mentioned before, is not used externallyuseYarn
: 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
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-scripts
Version or developer defined versionreact-scripts
getPackageName()
: Get the formalreact-scripts
The package namecheckIfOnline()
: Check whether the network connection is normalinstall()
: Installs the development dependency packagescheckNodeVersion()
Check:Node
Version informationsetCaretRangeForRuntimeDeps()
: Check whether the development dependency is installed correctly and the version is correctinit()
: 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 usednode
I 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:
appPath
Before:root
, the absolute path of the projectappName
: Project nameverbose
I’ve said this before,npm
Additional information at installation timeoriginalDirectory
: Original directory, the directory in which the command is executedtemplate
There 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!