eject.js 8.47 KB
Newer Older
Daniel Grant's avatar
Daniel Grant committed
1
// @remove-file-on-eject
eanplatter's avatar
eanplatter committed
2
3
4
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
Sophie Alpert's avatar
Sophie Alpert committed
5
6
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
eanplatter's avatar
eanplatter committed
7
 */
8
'use strict';
eanplatter's avatar
eanplatter committed
9

10
11
12
13
14
15
16
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
  throw err;
});

17
18
const fs = require('fs-extra');
const path = require('path');
19
const execSync = require('child_process').execSync;
20
21
22
const chalk = require('chalk');
const paths = require('../config/paths');
const createJestConfig = require('./utils/createJestConfig');
23
const inquirer = require('react-dev-utils/inquirer');
24
const spawnSync = require('react-dev-utils/crossSpawn').sync;
25
const os = require('os');
Daniel Grant's avatar
Daniel Grant committed
26

27
28
const green = chalk.green;
const cyan = chalk.cyan;
eanplatter's avatar
eanplatter committed
29

30
31
32
33
34
35
36
37
38
39
40
function getGitStatus() {
  try {
    let stdout = execSync(`git status --porcelain`, {
      stdio: ['pipe', 'pipe', 'ignore'],
    }).toString();
    return stdout.trim();
  } catch (e) {
    return '';
  }
}

41
42
43
44
45
46
47
48
49
50
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) {
      console.log(cyan('Close one! Eject aborted.'));
51
52
53
54
55
56
57
      return;
    }

    const gitStatus = getGitStatus();
    if (gitStatus) {
      console.error(
        chalk.red(
Joe Lim's avatar
Joe Lim committed
58
59
60
61
62
63
64
65
66
          '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(
67
            'Remove untracked files, stash or commit any changes, and try again.'
Joe Lim's avatar
Joe Lim committed
68
          )
69
      );
70
71
      process.exit(1);
    }
72

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
    console.log('Ejecting...');

    const ownPath = paths.ownPath;
    const appPath = paths.appPath;

    function verifyAbsent(file) {
      if (fs.existsSync(path.join(appPath, file))) {
        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);
      }
    }
Daniel Grant's avatar
Daniel Grant committed
89

90
    const folders = ['config', 'config/jest', 'scripts'];
91
92

    // Make shallow array of files paths
93
94
95
96
97
98
99
100
101
102
    const files = folders.reduce((files, folder) => {
      return files.concat(
        fs
          .readdirSync(path.join(ownPath, folder))
          // set full path
          .map(file => path.join(ownPath, folder, file))
          // omit dirs from file list
          .filter(file => fs.lstatSync(file).isFile())
      );
    }, []);
103
104
105
106
107

    // Ensure that the app folder is clean and we won't override any files
    folders.forEach(verifyAbsent);
    files.forEach(verifyAbsent);

108
109
110
111
    // Prepare Jest config early in case it throws
    const jestConfig = createJestConfig(
      filePath => path.posix.join('<rootDir>', filePath),
      null,
112
      paths.srcPaths
113
114
    );

115
116
117
118
119
120
121
122
123
124
125
126
127
128
    console.log();
    console.log(cyan(`Copying files into ${appPath}`));

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

    files.forEach(file => {
      let content = fs.readFileSync(file, 'utf8');

      // Skip flagged files
      if (content.match(/\/\/ @remove-file-on-eject/)) {
        return;
      }
129
130
131
132
133
134
135
136
137
138
139
140
141
      content =
        content
          // Remove dead code from .js files on eject
          .replace(
            /\/\/ @remove-on-eject-begin([\s\S]*?)\/\/ @remove-on-eject-end/gm,
            ''
          )
          // Remove dead code from .applescript files on eject
          .replace(
            /-- @remove-on-eject-begin([\s\S]*?)-- @remove-on-eject-end/gm,
            ''
          )
          .trim() + '\n';
142
143
144
145
146
147
148
149
150
151
      console.log(`  Adding ${cyan(file.replace(ownPath, ''))} to the project`);
      fs.writeFileSync(file.replace(ownPath, appPath), content);
    });
    console.log();

    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;
152
153
154
155
156
157
    if (appPackage.devDependencies) {
      // We used to put react-scripts in devDependencies
      if (appPackage.devDependencies[ownPackageName]) {
        console.log(`  Removing ${cyan(ownPackageName)} from devDependencies`);
        delete appPackage.devDependencies[ownPackageName];
      }
Daniel Grant's avatar
Daniel Grant committed
158
    }
159
    appPackage.dependencies = appPackage.dependencies || {};
160
161
162
    if (appPackage.dependencies[ownPackageName]) {
      console.log(`  Removing ${cyan(ownPackageName)} from dependencies`);
      delete appPackage.dependencies[ownPackageName];
Dan Abramov's avatar
Dan Abramov committed
163
    }
164
165
166
167
168
    Object.keys(ownPackage.dependencies).forEach(key => {
      // For some reason optionalDependencies end up in dependencies after install
      if (ownPackage.optionalDependencies[key]) {
        return;
      }
169
170
171
172
173
174
      console.log(`  Adding ${cyan(key)} to dependencies`);
      appPackage.dependencies[key] = ownPackage.dependencies[key];
    });
    // Sort the deps
    const unsortedDependencies = appPackage.dependencies;
    appPackage.dependencies = {};
175
176
177
178
179
    Object.keys(unsortedDependencies)
      .sort()
      .forEach(key => {
        appPackage.dependencies[key] = unsortedDependencies[key];
      });
180
    console.log();
181

182
183
184
    console.log(cyan('Updating the scripts'));
    delete appPackage.scripts['eject'];
    Object.keys(appPackage.scripts).forEach(key => {
185
      Object.keys(ownPackage.bin).forEach(binKey => {
186
        const regex = new RegExp(binKey + ' (\\w+)', 'g');
187
188
189
        if (!regex.test(appPackage.scripts[key])) {
          return;
        }
190
191
192
193
194
        appPackage.scripts[key] = appPackage.scripts[key].replace(
          regex,
          'node scripts/$1.js'
        );
        console.log(
195
196
197
          `  Replacing ${cyan(`"${binKey} ${key}"`)} with ${cyan(
            `"node scripts/${key}.js"`
          )}`
198
        );
199
      });
200
201
202
203
204
205
    });

    console.log();
    console.log(cyan('Configuring package.json'));
    // Add Jest config
    console.log(`  Adding ${cyan('Jest')} configuration`);
206
    appPackage.jest = jestConfig;
207
208
209

    fs.writeFileSync(
      path.join(appPath, 'package.json'),
210
      JSON.stringify(appPackage, null, 2) + os.EOL
211
212
213
214
215
216
217
218
219
220
221
222
223
224
    );
    console.log();

    // "Don't destroy what isn't ours"
    if (ownPath.indexOf(appPath) === 0) {
      try {
        // remove 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);
      } catch (e) {
        // It's not essential that this succeeds
      }
225
    }
226

227
    if (paths.useYarn) {
Dan Abramov's avatar
Dan Abramov committed
228
229
230
231
232
233
234
235
      const windowsCmdFilePath = path.join(
        appPath,
        'node_modules',
        '.bin',
        'react-scripts.cmd'
      );
      let windowsCmdFileContent;
      if (process.platform === 'win32') {
236
        // https://github.com/facebook/create-react-app/pull/3806#issuecomment-357781035
Dan Abramov's avatar
Dan Abramov committed
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
        // Yarn is diligent about cleaning up after itself, but this causes the react-scripts.cmd file
        // to be deleted while it is running. This trips Windows up after the eject completes.
        // We'll read the batch file and later "write it back" to match npm behavior.
        try {
          windowsCmdFileContent = fs.readFileSync(windowsCmdFilePath);
        } catch (err) {
          // If this fails we're not worse off than if we didn't try to fix it.
        }
      }

      console.log(cyan('Running yarn...'));
      spawnSync('yarnpkg', ['--cwd', process.cwd()], { stdio: 'inherit' });

      if (windowsCmdFileContent && !fs.existsSync(windowsCmdFilePath)) {
        try {
          fs.writeFileSync(windowsCmdFilePath, windowsCmdFileContent);
        } catch (err) {
          // If this fails we're not worse off than if we didn't try to fix it.
        }
      }
257
258
    } else {
      console.log(cyan('Running npm install...'));
259
260
261
      spawnSync('npm', ['install', '--loglevel', 'error'], {
        stdio: 'inherit',
      });
262
263
264
265
266
267
268
269
270
271
    }
    console.log(green('Ejected successfully!'));
    console.log();

    console.log(
      green('Please consider sharing why you ejected in this survey:')
    );
    console.log(green('  http://goo.gl/forms/Bi6CZjk1EqsdelXk1'));
    console.log();
  });