解读源码系列——create-react-app

266 阅读7分钟
原文链接: zhuanlan.zhihu.com

create-react-app 是一款优秀的react app脚手架,我花了一些时间把源码做了一个梳理和解读,用注释的方式把主线做了一个引导,大家可以一起学习下:

github项目地址:github.com/mutong77281…

该项目已经发布到npm上面,可以用于构建单页面应用

//安装single-app-cli
npm install single-app-cli -g

//创建一个单页面应用
single-app-cli <project-name>

这期解读了createapp的整个过程,下拉git模板项目的过程这期没有,后续会继续更新源码。

入口:packages/create-react-app/index.js

对应我的项目:index.js

#!/usr/bin/env node
/***/
'use strict';

var currentNodeVersion = process.versions.node;
var semer = currentNodeVersion.split('.');
var major = semer[0];

if (major < 8) {
 console.error('当前node 版本' + currentNodeVersion + '太低,请将node升级到8.0以上版本')
 process.exit(1);
}

require('./createApp.js');

主程序:packages/create-react-app/createReactApp.js

对应我的项目:createApp.js

这里引用了很多node自带的或者第三方的包,不要怂,从createApp这个方法开始看,这里开始我加了详细的注释,应该都可以看的懂。

#!/usr/bin/env node
/**
 * 
 * 
 * 注意:
 * 命令中:yarn 等同于 yarnpkg
 * process.exit(code)  code默认0, 0:正常退出,1:错误退出 
 */
'use strict';
const path = require('path');

/** nodejs命令行的完整解决方案,借鉴于ruby的commander   https://www.npmjs.com/package/commander*/
const commander = require('commander');

/**输出环境信息 https://www.npmjs.com/package/envinfo */
const envinfo = require('envinfo');
/* 定制输出字体颜色*/
const chalk = require('chalk');

/* fs的扩展库*/
const fs = require('fs-extra');

/**版本格式化和计算的库   https://www.npmjs.com/package/semver */
const semver = require('semver');

/** 交互式命令行工具 https://www.npmjs.com/package/inquirer  资料:https://www.jianshu.com/p/db8294cfa2f7 */
const inquirer = require('inquirer');

/** 用户临时文件资源的管理,在这里用 tmp-promise 代替   //https://www.npmjs.com/package/tmp   */
// const tmp = require('tmp');

/** 用来替代tmp功能  来做promise调用,tmp-promise :https://github.com/benjamingr/tmp-promise */
const tmpPromise = require('tmp-promise');

/** 将http请求转化为流输出 https://www.npmjs.com/package/hyperquest*/
const hyperquest = require('hyperquest');

/**压缩和解压  https://www.npmjs.com/package/tar-pack */
const unpack = require('tar-pack').unpack;

/**nodejs  spawn / spawnSync 的解决方案 https://www.npmjs.com/package/cross-spawn */
const spawn = require('cross-spawn');

/**检查package的name是否合法 */
const validateProjectName = require('validate-npm-package-name');

/** 在这里主要用到换行符 os.EOL*/
const os = require('os');

/** dns相关的api*/
const dns = require('dns');

/**url相关 */
const url = require('url');

const execSync = require('child_process').execSync;

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

/**在创建失败时保留,在新建时删除 */
const errorLogFilePatterns = [
    'npm-debug.log',
    'yarn-error.log',
    'yarn-debug.log',
];

let projectName;

const program = new commander.Command(packageJson.name)
    .version(packageJson.version)
    .arguments('<project-directory>')
    .usage(`${chalk.green('<project-directory>')} [options]`)
    .action(name => {
        projectName = name;
    })
    .option('--verbose', 'print additional logs')
    .option('--info', 'print environment debug info')
    .option(
        '--scripts-version <alternative-package>',
        'use a non-standard version of react-scripts'
    )
    .option('--use-npm')
    .option('--use-pnp')
    .option('--typescript')
    .allowUnknownOption()
    .on('--help', () => {
    })
    .parse(process.argv);

//如果键入 --info  输出信息
if (program.info) {
    envinfo
        .run(
            {
                System: ['OS', 'CPU'],
                Binaries: ['Node', 'npm', 'Yarn'],
                Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'],
                npmPackages: ['react', 'react-dom', 'react-scripts'],
                npmGlobalPackages: ['create-react-app'],
            },
            {
                duplicates: true,
                showNotFound: true,
            }
        )
        .then(console.log());
}

if (typeof projectName === 'undefined') {
    console.log(`${chalk.red('请键入项目文件夹名称')}`)
    process.exit(1);
}

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


createApp(
    projectName,
    program.verbose,
    program.scriptsVersion,
    program.useNpm,
    program.usePnp,
    program.typescript,
    hiddenProgram.internalTestingTemplate
);

/**创建app */
function createApp(
    name,
    verbose,
    version,
    useNpm,
    usePnp,
    useTypescript,
    template
) {
    const root = path.resolve(name);
    const appName = path.basename(root);

    /**
     * 检验应用名的合法性
     * */
    checkAppName(appName)

    //创建根目录
    fs.ensureDirSync(root);

    /**
     * 判断是否可以安全的创建项目
     */
    if (!isSafeToCreateProjectIn(root, name)) {
        process.exit(1);
    }

    console.log(`Create a new react app in ${chalk.green(root)}.`);
    console.log();
    //初始package配置
    const packageJson = {
        name: appName,
        version: '0.1.0',
        private: true
    }
    //写入基本配置项,   os.EOL  换行符
    fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(packageJson, null, 2) + os.EOL);

    /**判断使用 yarn 还是 npm */
    const useYarn = useNpm ? false : shouldUseYarn();

    //获取nodejs执行的目录
    const originalDirectory = process.cwd();
    //变更nodejs执行目录
    process.chdir(root);

    //如果不能使用yarn 或 npm  退出
    if (!useYarn && !checkThatNpmCanReadCwd()) {
        process.exit(1);
    }
    /**判断当前nodejs版本是否大于8.10.0,如果小于,则使用react-scripts@0.9.x */
    if (!semver.satisfies(process.version, '>=8.10.0')) {
        console.log(
            chalk.yellow(
                `你当前使用的nodejs版本 ${
                process.version
                } 过低,所以该项目将会使用较旧版本的工具来进行引导安装。\n\n` +
                `如果想要等到更高更完整的支持和体验,请升级nodejs到8.10.0或者更高的版本\n`
            )
        );
        // Fall back to latest supported react-scripts on Node 4
        version = 'react-scripts@0.9.x';
    }

    /**如果不实用yarn,即调用npm */
    if (!useYarn) {
        const npmInfo = checkNpmVersion();
        //如果npm的版本低于5.0.0,则使用react-scripts@0.9.x来进行引导安装
        if (!npmInfo.hasMinNpm) {
            if (npmInfo.npmVersion) {
                console.log(
                    chalk.yellow(
                        `你是用的npm版本 ${npmInfo.npmVersion}过低,所以项目只能使用较旧的工具进行引导安装.\n\n` +
                        `如果想要等到更高更完整的支持和体验,请升级npm到5及以上版本。\n`
                    )
                );
            }
            version = 'react-scripts@0.9.x';
        }
    }//该逻辑为是否开启yarn的pnp特性,pnp的好处可参考文献:https://loveky.github.io/2019/02/11/yarn-pnp/ 
    else if (usePnp) {
        const yarnInfo = checkYarnVersion();
        if (!yarnInfo.hasMinYarnPnp) {
            if (yarnInfo.yarnVersion) {
                console.log(
                    chalk.yellow(
                        `你是用的yarn版本 ${yarninfo.yarnVersion}过低,无法配合Plug'n'Play(pnp) 使用.\n\n` +
                        `如果想要等到更高更完整的支持和体验,请将yarn升级到1.12及以上版本。\n`
                    )
                );
            }
            usePnp = false;
        }
    }

    /**缓存依赖的版本信息,参考资料:https://yarnpkg.com/blog/2016/11/24/offline-mirror/ */
    if (useYarn) {
        let yarnUsesDefaultRegistry = true;
        try {
            //如果yarn的代理地址不是这个,那cache复制过去也没什么用
            yarnUsesDefaultRegistry = execSync('yarn config get registry')
                .toString().trim() === 'https://registry.yarnpkg.com';
        } catch (err) {

        }

        if (yarnUsesDefaultRegistry) {
            fs.copySync(
                require.resolve('./yarn.lock.cached'), //require.resolve(path1)===path.resolve(__dirname,path1),如果路径不存在,还会返回错误
                path.join(root, 'yarn.lock')
            )
        }
    }
    //  启动安装命令
    run(
        root,
        appName,
        version,
        verbose,
        originalDirectory,
        template,
        useYarn,
        usePnp,
        useTypescript
    );

}

/** 打印错误信息 */
function printValidationResults(results) {
    if (typeof results !== 'undefined') {
        results.forEach(error => {
            console.error(chalk.red(`  *  ${error}`));
        });
    }
}

/**检查应用名是否合法 */
function checkAppName(appName) {
    const validationResult = validateProjectName(appName);
    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);
    }

    // TODO: there should be a single place that holds the dependencies
    const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
    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);
    }
}

function isSafeToCreateProjectIn(root, name) {
    const validFiles = [
        '.DS_Store',
        'Thumbs.db',
        '.git',
        '.gitignore',
        '.idea',
        'README.md',
        'LICENSE',
        '.hg',
        '.hgignore',
        '.hgcheck',
        '.npmignore',
        'mkdocs.yml',
        'docs',
        '.travis.yml',
        '.gitlab-ci.yml',
        '.gitattributes',
    ];
    console.log();

    const conflicts = fs
        .readdirSync(root)
        .filter(file => !validFiles.includes(file))
        // IntelliJ IDEA creates module files before CRA is launched
        .filter(file => !/\.iml$/.test(file))
        // Don't treat log files from previous installation as conflicts
        .filter(
            file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0)
        );

    if (conflicts.length > 0) {
        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.'
        );

        return false;
    }

    // Remove any remnant files from a previous installation
    const currentFiles = fs.readdirSync(path.join(root));
    currentFiles.forEach(file => {
        errorLogFilePatterns.forEach(errorLogFilePattern => {
            // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files
            if (file.indexOf(errorLogFilePattern) === 0) {
                fs.removeSync(path.join(root, file));
            }
        });
    });
    return true;
}
/**判断yarn是否可用   yarnpkg 也可以改为 yarn */
function shouldUseYarn() {
    try {
        execSync('yarnpkg --version', { stdio: 'ignore' })
        return true;
    } catch (e) {
        return false;
    }
}
/**判断当前文件夹是否可以执行npm */
function checkThatNpmCanReadCwd() {
    return true;
}


/**检查npm版本信息 */
function checkNpmVersion() {
    let hasMinNpm = false;
    let npmVersion = null;
    try {
        npmVersion = execSync('npm --version').toString().trim();
        hasMinNpm = semver.gte(npmVersion, '5.0.0');
    }
    catch (err) {

    }
    return {
        hasMinNpm: hasMinNpm,
        npmVersion: npmVersion
    }
}

/**判断yarn的版本信息 */
function checkYarnVersion() {
    let hasMinYarnPnp = false;
    let yarnVersion = null;
    try {
        yarnVersion = execSync('yarn --version').toString().trim(); //yarn  可以用 yarnpkg 代替
        let trimmedYarnVersion = /^(.+?)[-+].+$/.exec(yarnVersion);
        if (trimmedYarnVersion) {
            trimmedYarnVersion = trimmedYarnVersion.pop();// pop 删除数组最后一个元素,并返回
        }
        hasMinYarnPnp = semver.gte(trimmedYarnVersion || yarnVersion, '1.12.0');
    } catch (err) {

    }
    return {
        hasMinYarnPnp: hasMinYarnPnp,
        yarnVersion: yarnVersion
    }
}


function run(
    root,
    appName,
    version,
    verbose,
    originalDirectory,
    template,
    useYarn,
    usePnp,
    useTypescript
) {
    //获取需要安装的package
    getInstallPackage(version, originalDirectory)
        .then(packageToInstall => {
            const allDependencies = ['react', 'react-dom', packageToInstall];
            /**如果用ts语法,还需要安装下面的包 */
            if (useTypescript) {
                allDependencies.push(
                    // TODO: get user's node version instead of installing latest
                    '@types/node',
                    '@types/react',
                    '@types/react-dom',
                    // TODO: get version of Jest being used instead of installing latest
                    '@types/jest',
                    'typescript'
                );
            }
            console.log('开始安装依赖包,这可能需要几分钟的时间');
            /*获取安装包的包名,检查yarn的服务是否通畅*/
            getPackageName(packageToInstall)
                .then(packageName =>
                    checkIfOnline(useYarn)
                        .then(isOnline => ({
                            isOnline: isOnline,
                            packageName: packageName
                        }))
                )
                .then(info => {
                    const isOnline = info.isOnline;
                    const packageName = info.packageName;
                    console.log(
                        `安装 ${chalk.cyan('react')}, ${chalk.cyan(
                            'react-dom'
                        )}, 和 ${chalk.cyan(packageName)}...`
                    );
                    console.log();

                    /**安装所有依赖包 */
                    return install(root, useYarn, usePnp, allDependencies, verbose, isOnline)
                        .then(() => packageName);
                })
                .then(async packageName => {
                    /**检查当前的nodejs版本是否符合依赖包的要求 */
                    checkNodeVersion(packageName);

                    /**设置运行时依赖包[react,react-dom]的大版本锁定,检查react-scripts的版本信息 */
                    setCaretRangeForRuntimeDeps(packageName);

                    const pnpPath = path.join(process.cwd(), '.png.js');
                    const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
                    await executeNodeScript(
                        { cwd: process.cwd(), args: nodeArgs },
                        [root, appName, verbose, originalDirectory, template],
                        `
                        var init = require('${packageName}/scripts/init.js');
                        init.apply(null, JSON.parse(process.argv[1]));
                      `)

                    if (version === 'react-scripts@0.9.x') {
                        //还是一样的信息,不翻译了
                        console.log(
                            chalk.yellow(
                                `\nNote: the project was bootstrapped with an old unsupported version of tools.\n` +
                                `Please update to Node >=8.10 and npm >=5 to get supported tools in new projects.\n`
                            )
                        );
                    }


                })
                .catch(reason => {
                    console.log();
                    console.log('Aborting installation.');
                    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();

                    /**失败退出时删除相应的文件 */
                    const knownGeneratedFiles = [
                        'package.json',
                        'yarn.lock',
                        'node_modules',
                    ];

                    const currentFiles = fs.readdirSync(path.join(root));

                    currentFiles.forEach(file => {
                        knownGeneratedFiles.forEach(fileToMatch => {
                            if (file === fileToMatch) {
                                console.log('正在删除生成的文件...')
                                fs.removeSync(path.join(root, file));
                            }
                        })
                    })

                    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.rmdirSync(path.join(root));
                    }
                    console.log('删除操作完毕');
                    process.exit(0);
                })
        })

}

/**将包名和版本号做拼接 */
function getInstallPackage(version, originalDirectory) {
    let packageToInstall = 'react-scripts';
    //先格式化版本信息,不合格返回null
    const validSemver = semver.valid(version);
    if (validSemver) {
        //标准版本号
        packageToInstall += `@${validSemver}`
    } else if (version) {
        //不标准版本号
        if (version[0] === '@' && version.indexOf('/') === -1) {
            packageToInstall += version;
        } else if (version.match(/^file:/)) {
            //文件
            packageToInstall = `file:${path.resolve(
                originalDirectory,
                version.match(/^file:(.*)?$/)[1]
            )}`;
        } else {
            // 针对tar.gz及可选路径
            packageToInstall = version;
        }
    }

    const scriptsToWarn = [
        {
            name: 'react-scripts-ts',
            message: chalk.yellow(
                'react-scripts-ts包已经被废弃. 新建的React项目中已经可以支持TypeScript原生语法. 你可以用 --typescriptwhen 来生成支持Typescript语法的app,还想继续使用react-scripts-ts吗?'
            ),
        },
    ];

    /**这一段判断感觉有些多余,永远是false */
    for (const script of scriptsToWarn) {
        if (packageToInstall.startsWith(script.name)) {
            inquirer
                .prompt({
                    type: 'confirm',
                    name: 'useScript',
                    message: script.message,
                    default: false,
                })
                .then(answer => {
                    if (!answer.useScript) {
                        process.exit(0);
                    }
                    return packageToInstall;
                })
        }
    }

    return Promise.resolve(packageToInstall);

}


/**获取依赖包的包名 
 * 1.先做文件解压,通过package.json来获取包名
 * 2.文件解压失败,通过路径正则匹配来获取包名,
 * 可以用这个包名来做下面的正则测试:https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz
*/
function getPackageName(installPackage) {
    //判断是否为资源文件压缩包
    if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
        //创建临时文件夹来存放包文件
        getTemporaryDirectory()
            .then(obj => {
                let stream;
                if (/^http/.test(installPackage)) {
                    stream = hyperquest(installPackage);
                } else {
                    stream = fs.createReadStream(installPackage);
                }
                return extractStream(stream, obj.tmpdir)
                    .then(() => obj)

            })
            .then(obj => {
                const packageName = require(path.join(obj.tmpdir, 'package.json')).name;
                obj.cleanup();
                console.log(packageName);
                return packageName;
            })
            .catch(err => {
                console.log(
                    `无法通过解压文件来获取包名,错误为: ${err.message}`
                );
                const assumedProjectName = installPackage.match(/^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/)[1];
                console.log(
                    `通过文件名,我们推算出包名为:${chalk.cyan(
                        assumedProjectName
                    )}"`
                );
                return Promise.resolve(assumedProjectName);
            })
    } /**包路径为git地址 */
    else if (installPackage.indexOf('git+') === 0) {
        // Pull package name out of git urls e.g:
        // git+https://github.com/mycompany/react-scripts.git
        // git+ssh://github.com/mycompany/react-scripts.git#v1.2.3
        return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]);
    } else if (installPackage.match(/.+@/)) {
        // Do not match @scope/ when stripping off @version or @tag
        return Promise.resolve(
            installPackage.charAt(0) + installPackage.substr(1).split('@')[0]
        );
    } 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 Promise.resolve(installPackage);
}


/**创建和获取临时文件夹,进程退出后会被自动清理调,
 * 原包用的tmp,在这里我替换成了tmp-promise */
function getTemporaryDirectory() {

    return tmpPromise.dir({ unsafeCleanup: true })
        .then(o => (
            {
                tmpdir: o.path,
                cleanup: () => {
                    try {
                        o.cleanup()
                    } catch (err) {

                    }
                }
            }
        ))
}

/**解压文件流到文件夹下 */
function extractStream(stream, dest) {
    return new Promise((resolve, reject) => {
        stream.pipe(
            unpack(dest, err => {
                if (err) {
                    return reject(err);
                }
                resolve(dest);
            })
        )
    })
}

/**检查yarn是否在线,ping一下yarn的主机,有代理就ping代理的主机 */
function checkIfOnline(useYarn) {
    if (!useYarn) {
        return Promise.resolve(true);
    }

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

/**获取代理信息 */
function getProxy() {
    if (process.env.https_proxy) {
        return process.env.https_proxy;
    } else {
        try {
            let httpsProxy = process.execSync('npm config get https-proxy').toString().trim();
            return httpsProxy !== 'null' ? httpsProxy : undefined;
        }
        catch (err) {
            return;
        }
    }
}

/**安装依赖包
 * root 项目文件夹
 * useYarn 是否使用yarn
 * usePnp 是否使用pnp特性
 * dependencies 依赖
 * verbose  详细信息输出
 * isOnline 是否正常请求yarn服务
 */
function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
    return new Promise((resolve, reject) => {
        let command;
        let args;
        if (useYarn) {
            command = 'yarnpkg';
            args = ['add', '--exact'];

            if (!isOnline) {
                args.push('--offline');
            }
            if (usePnp) {
                args.push('--enable-pnp');
            }

            /**兼容性好 */
            [].push.apply(args, dependencies);

            /**将nodejs的工作进程指定到当前目录,
             * 这样做是为了解决下面这个bug 
             * https://github.com/facebook/create-react-app/issues/3326.
             * */
            args.push('--cwd');
            args.push(root);

            if (!isOnline) {
                console.log(chalk.yellow('你好像掉线了'));
                console.log(chalk.yellow('我们将使用本地的yarn缓存来安装'));
                console.log();
            }
        }
        else {
            command = 'npm';
            args = [
                'install',
                '--save',
                '--save-exact', //精确安装指定版本
                '--loglevel',
                'error',
            ].concat(dependencies);

            if (usePnp) {
                console.log(chalk.yellow("NPM 不支持 PnP."));
                console.log(chalk.yellow('我们将会使用常规方式继续安装'));
                console.log();
            }
        }

        if (verbose) {
            args.push('--verbose');
        }

        /**stdio:选项用于配置在父进程和子进程之间建立的管道,设置为inherit则会继承父进程的输入输出流 */
        const child = spawn(command, args, { stdio: 'inherit' });
        child.on('close', code => {
            if (code !== 0) {
                reject({ command: `${command} ${args.join(' ')}` })
                return;
            }
            resolve();
        });
    });
}

/**检测node版本是否相合,
 * 依赖包的package.json中的node版本和本地运行的node 版本做比较*/
function checkNodeVersion(packageName) {
    const packageJsonPath = path.resolve(process.cwd(), 'node_modules', packageName, 'package.json');
    if (!fs.existsSync(packageJsonPath)) {
        return;
    }

    const packageJson = require(packageJsonPath);
    if (!packageJson.engines || !packageJson.engines.node) {
        return;
    }
    const range = packageJson.engines.node;
    if (!semver.satisfies(process.version, range)) {
        console.error('您当前的node版本为:%s,创建一个react应用需要%s或者更高的版本,请更新你的node版本', process.version, range);
        process.exit(1);
    }


}

/**
 * 补充锁定运行时依赖包的大版本信息,
 * 检查依赖包的版本号信息是否正常
 */
function setCaretRangeForRuntimeDeps(packageName) {
    let packagePath = path.join(process.cwd(), 'package.json');
    let packageJson = require(packagePath);
    if (typeof packageJson.dependencies === 'undefined') {
        console.error(`${chalk.red('无法在package.json中找到依赖包配置')}`);
        process.exit(1);
    }

    if (typeof packageJson.dependencies[packageName] === 'undefined') {
        console.error(chalk.red(`无法找到${packageName}配置项`));
        process.exit(1);
    }

    makeCaretRange(packageJson.dependencies, 'react');
    makeCaretRange(packageJson.dependencies, 'react-dom');

    fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + os.EOL);
}


/**补充版本号信息,就是追加一个^,用于锁定大版本信息不可被修改 */
function makeCaretRange(dependencies, name) {
    const version = dependencies[name];
    if (typeof version === 'undefined') {
        console.error(chalk.red(`package.json中缺少${name}包的依赖`));
        process.exit(1);
    }
    let patchedVersion = `^${version}`;
    //检查填充完的版本号信息是否合法
    if (!semver.validRange(patchedVersion)) {
        console.error(chalk.red(`包${name}的版本号${version}无法进行补充,因为,补充完之后的版本号${patchedVersion}会变得不可用`));
        patchedVersion = version;
    }
    dependencies[name] = patchedVersion;

}

/**
 * cwd:当前工作目录
 * args:参数
 * data
 * source 执行的一段js
 * 
 * spawn中参数含义:
 * process.execPath 当前可执行程序:...path/node.exe
 * ...args 业务配置参数 --require .pnp.js
 * -e stirng :-e, --eval "script" 执行一段js,比如 -e ‘console.log("hello node")'
 * -- JSON.stringify(data)  -- [path.resolve(projectName),projectName,--verbose,...]
 * {cwd,stdio}  cwd:将路径定位到的目录,这样arg的js访问路径可以写成相对的。
 */
function executeNodeScript({ cwd, args }, data, source) {

    return new Promise((resolve, reject) => {
        const child = spawn(
            process.execPath,
            [...args, '-e', source, '--', JSON.stringify(data)],
            { cwd, stdio: 'inherit' }
        )

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