This repository has been archived by the owner on Jan 18, 2024. It is now read-only.
/
Eject.js
353 lines (307 loc) · 11.4 KB
/
Eject.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
// @flow
import chalk from 'chalk';
import fse from 'fs-extra';
import matchRequire from 'match-require';
import path from 'path';
import spawn from 'cross-spawn';
import { ProjectUtils, Detach, Versions } from 'xdl';
import log from '../../log';
import prompt from '../../prompt';
import { loginOrRegisterIfLoggedOut } from '../../accounts';
export async function ejectAsync(projectRoot: string, options) {
const filesWithExpo = await filesUsingExpoSdk(projectRoot);
const usingExpo = filesWithExpo.length > 0;
let expoSdkWarning;
if (usingExpo) {
expoSdkWarning = `${chalk.bold(
'Warning!'
)} We found at least one file where your project imports the Expo SDK:
`;
for (let filename of filesWithExpo) {
expoSdkWarning += ` ${chalk.cyan(filename)}\n`;
}
expoSdkWarning += `
${chalk.yellow.bold(
'If you choose the "plain" React Native option below, these imports will stop working.'
)}`;
} else {
expoSdkWarning = `\
We didn't find any uses of the Expo SDK in your project, so you should be fine to eject to
"Plain" React Native. (This check isn't very sophisticated, though.)`;
}
log(
`
${expoSdkWarning}
We ${chalk.italic('strongly')} recommend that you read this document before you proceed:
${chalk.cyan(
'https://github.com/react-community/create-react-native-app/blob/master/EJECTING.md'
)}
Ejecting is permanent! Please be careful with your selection.
`
);
let reactNativeOptionMessage = "React Native: I'd like a regular React Native project.";
if (usingExpo) {
reactNativeOptionMessage =
chalk.italic(
"(WARNING: See above message for why this option may break your project's build)\n "
) + reactNativeOptionMessage;
}
const questions = [
{
type: 'list',
name: 'ejectMethod',
message: 'How would you like to eject from create-react-native-app?',
default: usingExpo ? 'expoKit' : 'plain',
choices: [
{
name: reactNativeOptionMessage,
value: 'plain',
},
{
name:
"ExpoKit: I'll create or log in with an Expo account to use React Native and the Expo SDK.",
value: 'expoKit',
},
{
name: "Cancel: I'll continue with my current project structure.",
value: 'cancel',
},
],
},
];
const ejectMethod =
options.ejectMethod ||
(await prompt(questions, {
nonInteractiveHelp:
'Please specify eject method (expoKit, plain) with --eject-method option.',
})).ejectMethod;
if (ejectMethod === 'plain') {
const useYarn = await fse.exists(path.resolve('yarn.lock'));
const npmOrYarn = useYarn ? 'yarn' : 'npm';
const { configPath, configName } = await ProjectUtils.findConfigFileAsync(projectRoot);
const { exp, pkg: pkgJson } = await ProjectUtils.readConfigJsonAsync(projectRoot);
const appJson = configName === 'app.json' ? JSON.parse(await fse.readFile(configPath)) : {};
if (!exp) throw new Error(`Couldn't read ${configName}`);
if (!pkgJson) throw new Error(`Couldn't read package.json`);
let { displayName, name } = appJson;
if (!displayName || !name) {
log("We have a couple of questions to ask you about how you'd like to name your app:");
({ displayName, name } = await prompt(
[
{
name: 'displayName',
message: "What should your app appear as on a user's home screen?",
default: name || exp.name,
validate: s => {
return s.length > 0;
},
},
{
name: 'name',
message: 'What should your Android Studio and Xcode projects be called?',
default: pkgJson.name ? stripDashes(pkgJson.name) : undefined,
validate: s => {
return s.length > 0 && s.indexOf('-') === -1 && s.indexOf(' ') === -1;
},
},
],
{
nonInteractiveHelp: 'Please specify "displayName" and "name" in app.json.',
}
));
appJson.displayName = displayName;
appJson.name = name;
}
delete appJson.expo;
log('Writing app.json...');
// write the updated app.json file
await fse.writeFile(path.resolve('app.json'), JSON.stringify(appJson, null, 2));
log(chalk.green('Wrote to app.json, please update it manually in the future.'));
const ejectCommand = 'node';
const ejectArgs = [
path.resolve('node_modules', 'react-native', 'local-cli', 'cli.js'),
'eject',
];
const { status } = spawn.sync(ejectCommand, ejectArgs, {
stdio: 'inherit',
});
if (status !== 0) {
log(chalk.red(`Eject failed with exit code ${status}, see above output for any issues.`));
log(chalk.yellow('You may want to delete the `ios` and/or `android` directories.'));
process.exit(1);
} else {
log('Successfully copied template native code.');
}
const newDevDependencies = [];
// Try to replace the Babel preset.
try {
const projectBabelPath = path.resolve('.babelrc');
// If .babelrc doesn't exist, the app is using the default config and
// editing the config is not necessary.
if (await fse.exists(projectBabelPath)) {
const projectBabelRc = (await fse.readFile(projectBabelPath)).toString();
// We assume the .babelrc is valid JSON. If we can't parse it (e.g. if
// it's JSON5) the error is caught and a message asking to change it
// manually gets printed.
const babelRcJson = JSON.parse(projectBabelRc);
if (babelRcJson.presets && babelRcJson.presets.includes('babel-preset-expo')) {
babelRcJson.presets = babelRcJson.presets.map(preset =>
preset === 'babel-preset-expo'
? 'babel-preset-react-native-stage-0/decorator-support'
: preset
);
await fse.writeFile(projectBabelPath, JSON.stringify(babelRcJson, null, 2));
newDevDependencies.push('babel-preset-react-native-stage-0');
log(
chalk.green(
`Babel preset changed to \`babel-preset-react-native-stage-0/decorator-support\`.`
)
);
}
}
} catch (e) {
log(
chalk.yellow(
`We had an issue preparing your .babelrc for ejection.
If you have a .babelrc in your project, make sure to change the preset
from \`babel-preset-expo\` to \`babel-preset-react-native-stage-0/decorator-support\`.`
)
);
log(chalk.red(e));
}
delete pkgJson.main;
// NOTE: expo won't work after performing a plain eject, so we should delete this
// it will be a better error message for the module to not be found than for whatever problems
// missing native modules will cause
delete pkgJson.dependencies.expo;
if (pkgJson.devDependencies) {
delete pkgJson.devDependencies['react-native-scripts'];
delete pkgJson.devDependencies['jest-expo'];
}
if (!pkgJson.scripts) {
pkgJson.scripts = {};
}
pkgJson.scripts.start = 'react-native start';
pkgJson.scripts.ios = 'react-native run-ios';
pkgJson.scripts.android = 'react-native run-android';
if (pkgJson.jest !== undefined) {
newDevDependencies.push('jest');
if (pkgJson.jest.preset === 'jest-expo') {
pkgJson.jest.preset = 'react-native';
} else {
log(
`${chalk.bold(
'Warning'
)}: it looks like you've changed the Jest preset from jest-expo to ${
pkgJson.jest.preset
}. We recommend you make sure this Jest preset is compatible with ejected apps.`
);
}
}
// no longer relevant to an ejected project (maybe build is?)
delete pkgJson.scripts.eject;
log(`Updating your ${npmOrYarn} scripts in package.json...`);
await fse.writeFile(path.resolve('package.json'), JSON.stringify(pkgJson, null, 2));
log(chalk.green('Your package.json is up to date!'));
// Starting from react-native 0.49.x (SDK 22), react-native eject template includes this out of the box.
if (!Versions.gteSdkVersion(exp, '22.0.0')) {
log(`Adding entry point...`);
const lolThatsSomeComplexCode = `import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('${appJson.name}', () => App);
`;
await fse.writeFile(path.resolve('index.js'), lolThatsSomeComplexCode);
log(chalk.green('Added new entry points!'));
}
log(`
Note that using \`${npmOrYarn} start\` will now require you to run Xcode and/or
Android Studio to build the native code for your project.`);
log('Removing node_modules...');
await fse.remove('node_modules');
if (useYarn) {
log('Installing packages with yarn...');
const args = newDevDependencies.length > 0 ? ['add', '--dev', ...newDevDependencies] : [];
spawn.sync('yarnpkg', args, { stdio: 'inherit' });
} else {
// npm prints the whole package tree to stdout unless we ignore it.
const stdio = [process.stdin, 'ignore', process.stderr];
log('Installing existing packages with npm...');
spawn.sync('npm', ['install'], { stdio });
if (newDevDependencies.length > 0) {
log('Installing new packages with npm...');
spawn.sync('npm', ['install', '--save-dev', ...newDevDependencies], {
stdio,
});
}
}
} else if (ejectMethod === 'expoKit') {
await loginOrRegisterIfLoggedOut();
await Detach.detachAsync(projectRoot, options);
} else if (ejectMethod === 'cancel') {
// we don't want to print the survey for cancellations
log('OK! If you change your mind you can run this command again.');
return;
} else {
throw new Error(
`Unrecognized eject method "${ejectMethod}". Valid options are: expoKit, plain.`
);
}
log(
`${chalk.green('Ejected successfully!')}
Please consider letting us know why you ejected in this survey:
${chalk.cyan('https://goo.gl/forms/iD6pl218r7fn9N0d2')}`
);
}
async function filesUsingExpoSdk(projectRoot: string): Promise<Array<string>> {
const projectJsFiles = await findJavaScriptProjectFilesInRoot(projectRoot);
const jsFileContents = (await Promise.all(projectJsFiles.map(f => fse.readFile(f)))).map(
(buf, i) => {
return {
filename: projectJsFiles[i],
contents: buf.toString(),
};
}
);
const filesUsingExpo = [];
for (let { filename, contents } of jsFileContents) {
const requires = matchRequire.findAll(contents);
if (requires.includes('expo')) {
filesUsingExpo.push(filename);
}
}
filesUsingExpo.sort();
return filesUsingExpo;
}
function stripDashes(s: string): string {
let ret = '';
for (let c of s) {
if (c !== ' ' && c !== '-') {
ret += c;
}
}
return ret;
}
async function findJavaScriptProjectFilesInRoot(root: string): Promise<Array<string>> {
// ignore node_modules
if (root.includes('node_modules')) {
return [];
}
const stats = await fse.stat(root);
if (stats.isFile()) {
if (root.endsWith('.js')) {
return [root];
} else {
return [];
}
} else if (stats.isDirectory()) {
const children = await fse.readdir(root);
// we want to do this concurrently in large project folders
const jsFilesInChildren = await Promise.all(
children.map(f => findJavaScriptProjectFilesInRoot(path.join(root, f)))
);
return [].concat.apply([], jsFilesInChildren);
} else {
// lol it's not a file or directory, we can't return a honey badger, 'cause it don't give a
return [];
}
}