preface

NPX create-react-app [filename] : NPX create-react-app [filename] : NPX create-react-app [filename] : NPX create-react-app [filename] : NPX create-react-app Discovery You can use Yarn eject to display related configurations for custom configurations.

So I wanted to know what eject does, and there’s a lot of stuff in there, a lot of stuff THAT I hadn’t touched on before, and since I looked at the source code for Eject and build, I think there’s a lot more we can do.

Initialization statement

In fact, most of the content is based on Node to implement:

If you are node xiaobai, you can learn some knowledge about Node.

If you’re a Node boss, see if there are any ideas you can learn from.

If you think something is wrong, or the explanation is not clear enough, ask the leaders to point it outCopy the code

Subscribe to Promise’s Reject

process.on("unhandledRejection", err => {
  throw err;
});
Copy the code

When yarn Reject is initialized, a subscription to unhandledRejection is published. This subscription is made when, in a round of the event loop, a Promise is rejected and the Promise is not bound to an error handler. The unhandledRejection event will be emitted.

Throw Err (rejected); throw Err (rejected);

Since this is subscribed, the Node process will be directly exited if rejected occurs in the future.

Declare the method to use (initialization)

const fs = require('fs-extra'); // The fs extension in Node, in addition to supporting all the FS apis, also supports the Promise writing method
const path = require('path'); // Get the directory module
const execSync = require('child_process').execSync; // Run the synchronization command
const chalk = require('react-dev-utils/chalk'); // Change the color of the log font
const paths = require('.. /config/paths'); // Handle the path
const createJestConfig = require('./utils/createJestConfig'); // Create a unit test configuration
const inquirer = require('react-dev-utils/inquirer'); // A collection of commonly used interactive command-line user interfaces
const spawnSync = require('react-dev-utils/crossSpawn').sync; // Execute system commands cross-platform
const os = require('os'); // The method used to operate the system

const green = chalk.green; / / green
const cyan = chalk.cyan; / / cyan

function getGitStatus() { // Get git status
  try {
    let stdout = execSync(`git status --porcelain`, {
      stdio: ['pipe'.'pipe'.'ignore'],
    }).toString();
    return stdout.trim();
  } catch (e) {
    return ' '; }}function tryGitAdd(appPath) { // This is used to submit changes or updates in the root config and scripts folders, but not deleted parts
  try {
    spawnSync(
      'git'['add', path.join(appPath, 'config'), path.join(appPath, 'scripts')] and {stdio: 'inherit'});return true;
  } catch (e) {
    return false; }}// A message explaining that TS, SASS, and CSS are supported without eject
console.log(
  chalk.cyan.bold(
    'NOTE: Create React App 2+ supports TypeScript, Sass, CSS Modules and more without ejecting: ' +
      'https://reactjs.org/blog/2018/10/01/create-react-app-v2.html'));Copy the code

Popup webPack configuration (core)

inquirer.prompt({
    type: "confirm".name: "shouldEject".message: "Are you sure you want to eject? This action is permanent.".default: false
  }).then(answer= > {
    if(! answer.shouldEject) {/ / n
      console.log(cyan("Close one! Eject aborted."));
      return;
    }
    / *... * /
  })
Copy the code

Here you need to manually enter y or N to perform the corresponding processing according to the state selected by the developer. The effect is as follows:

ShouldEject property, which is the value of the name property, shouldEject is true when the developer puts in y, shouldEject is false if the developer puts in n

When shouldEject is false, the developer has chosen not to pop the eject configuration

If y is selected, the following steps are performed

Check the file status of the current project

const gitStatus = getGitStatus();
if (gitStatus) {
  console.error(
    chalk.red(
      "This git repository has untracked files or uncommitted changes:"
    ) +
      "\n\n" +
      gitStatus
        .split("\n")
        .map(line= > line.match(/ .*/g) [0].trim())
        .join("\n") +
      "\n\n" +
      chalk.red(
        "Remove untracked files, stash or commit any changes, and try again.")); process.exit(1);
}
Copy the code

New files or uncommitted files are listed in the current Git repository, which will interrupt the current Node process in order to prevent the files to be ejected from conflict or overwriting

To be on the safe side, you want developers to make sure that there are no new or modified files in your current Git repository

Check to see if the file you want to pop up has the current project

console.log("Ejecting...");

const ownPath = paths.ownPath; // The parent directory of the current file
const appPath = paths.appPath; // Current directory

function verifyAbsent(file) {
  if (fs.existsSync(path.join(appPath, file))) {
    // Check whether the file exists
    console.error(
      ` \ `${file}\` already exists in your app folder. We cannot ` +
        "continue as you would lose all the changes in that file or directory. " +
        "Please move or delete it (maybe make a copy for backup) and run this " +
        "command again."
    );
    process.exit(1); }}const folders = ["config"."config/jest"."scripts"];

// Create shallow file paths
const files = folders.reduce((files, folder) = > {
  return files.concat(
    fs
      .readdirSync(path.join(ownPath, folder))
      //node_modules/react-scripts/config
      .map(file= > path.join(ownPath, folder, file))
      //node_modules/react-scripts/config/env.js
      .filter(file= > fs.lstatSync(file).isFile())
    // Check that the current directory is of file type
    // all files under config, all files under config/jest, all files under scripts (excluding utils)); } []);// Check whether all files exist
folders.forEach(verifyAbsent);
files.forEach(verifyAbsent);
Copy the code

Since the files under these two folders will be popped up later, check to see if the two folders exist in the root directory of the current project and if the same files exist

If so, you want to remove or delete the file, as above, and then execute the command again

Create folders in the root directory

folders.forEach(folder= > {
  fs.mkdirSync(path.join(appPath, folder));
});
Copy the code

Create the corresponding folder in the root directory

Reading file contents

files.forEach(file= > {
  let content = fs.readFileSync(file, "utf8"); // Read the contents of the file

  // Skip the marked file
  if (content.match(/\/\/ @remove-file-on-eject/)) {
    return;
  }
  content =
    content
      .replace(
        /\/\/ @remove-on-eject-begin([\s\S]*?) \/\/ @remove-on-eject-end/gm.""
      )
      .replace(
        /-- @remove-on-eject-begin([\s\S]*?) -- @remove-on-eject-end/gm.""
      )
      .trim() + "\n";
  console.log(`  Adding ${cyan(file.replace(ownPath, ""))} to the project`);
  fs.writeFileSync(file.replace(ownPath, appPath), content);
});
Copy the code

Read the contents of all files, if there is a @remove-file-on-eject file, skip directly, do not create write

If a file has content beginning with //@remove-on-eject-begin and ending with //@remove-on-eject-end, delete it and write the rest

Update the rely on

const ownPackage = require(path.join(ownPath, "package.json"));
const appPackage = require(path.join(appPath, "package.json"));

console.log(cyan("Updating the dependencies"));
const ownPackageName = ownPackage.name;
if (appPackage.devDependencies) {
  // We put the React script in devDependencies
  if (appPackage.devDependencies[ownPackageName]) {
    console.log(`  Removing ${cyan(ownPackageName)} from devDependencies`);
    delete appPackage.devDependencies[ownPackageName];
  }
}
appPackage.dependencies = appPackage.dependencies || {};
if (appPackage.dependencies[ownPackageName]) {
  console.log(`  Removing ${cyan(ownPackageName)} from dependencies`);
  delete appPackage.dependencies[ownPackageName];
}
Object.keys(ownPackage.dependencies).forEach(key= > {
  // For some reason, optionalDependencies end as dependencies after installation
  if (ownPackage.optionalDependencies[key]) {
    return;
  }
  console.log(`  Adding ${cyan(key)} to dependencies`);
  appPackage.dependencies[key] = ownPackage.dependencies[key];
});
// Sort the deps
const unsortedDependencies = appPackage.dependencies;
appPackage.dependencies = {};
Object.keys(unsortedDependencies)
  .sort()
  .forEach(key= > {
    appPackage.dependencies[key] = unsortedDependencies[key];
  });
console.log();

console.log(cyan("Updating the scripts"));
delete appPackage.scripts["eject"];
Object.keys(appPackage.scripts).forEach(key= > {
  Object.keys(ownPackage.bin).forEach(binKey= > {
    const regex = new RegExp(binKey + " (\\w+)"."g");
    if(! regex.test(appPackage.scripts[key])) {return;
    }
    appPackage.scripts[key] = appPackage.scripts[key].replace(
      regex,
      "node scripts/$1.js"
    );
    console.log(
      `  Replacing ${cyan(`"${binKey} ${key}"`)} with ${cyan(
        `"node scripts/${key}.js"`
      )}`
    );
  });
});

console.log();
console.log(cyan("Configuring package.json"));
// Add jest configuration
console.log(`  Adding ${cyan("Jest")} configuration`);
appPackage.jest = jestConfig;

// Add the Babel configuration
console.log(`  Adding ${cyan("Babel")} preset`);
appPackage.babel = {
  presets: ["react-app"]};// Add the ESLint configuration
console.log(`  Adding ${cyan("ESLint")} configuration`);
appPackage.eslintConfig = {
  extends: "react-app"
};

fs.writeFileSync(
  path.join(appPath, "package.json"),
  JSON.stringify(appPackage, null.2) + os.EOL
); // write json, 2 for formatting indentation
Copy the code

There seems to be a lot of code here, but there is no very complex knowledge point, so I will not do a detailed introduction, you will understand

Deal with what happens after the popup (sweep)

Here, in fact, the work of popup corresponding to the file has been completed, but here needs to pop up after the project has no relation to the resources can be cleaned up

Remove the react-scripts correlation from the declaration file

if (fs.existsSync(paths.appTypeDeclarations)) {
  try {
    // Read the application declaration file
    let content = fs.readFileSync(
      paths.appTypeDeclarations,
      "utf8"
    );
    const ownContent =
      fs
        .readFileSync(paths.ownTypeDeclarations, "utf8")
        .trim() + os.EOL;

    // Remove the react-scripts references because they are getting copies of the types in the project
    content =
      content
        // Delete the react-scripts type
        .replace(
          /^\s*\/\/\/\s*
      .*(? :\n|$)/gm."") .trim() + os.EOL; fs.writeFileSync( paths.appTypeDeclarations, (ownContent + os.EOL + content).trim() + os.EOL ); }}Copy the code

Remove react-scripts correlation from the root node_modules directory

if (ownPath.indexOf(appPath) === 0) {
  try {
    // Remove the react-scripts and react-scripts binaries from app node_modules
    Object.keys(ownPackage.bin).forEach(binKey= > {
      fs.removeSync(path.join(appPath, "node_modules".".bin", binKey)); }); fs.removeSync(ownPath); }}Copy the code

conclusion

That’s about all the eject code I’ve covered so far. Actually, it looks like a lot of code, but if you look at it, it’s not that complicated. It’s just that there’s a lot of node stuff in there, and it’s not very user-friendly for the front-end class

But as long as you query the corresponding API, you will find that it is not difficult to implement, but for some ideas to achieve this approach, it is worth learning

How react hides webpack configuration and how react pops it up will help us write a NPM package that does the same thing

Hope this article can help you, and more likes, thanks