This repository has been archived by the owner on Jan 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 477
/
Eject.ts
376 lines (329 loc) · 12.3 KB
/
Eject.ts
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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
import * as ConfigUtils from '@expo/config';
import JsonFile from '@expo/json-file';
import { Detach, Exp, Versions } from '@expo/xdl';
import chalk from 'chalk';
import fse from 'fs-extra';
import npmPackageArg from 'npm-package-arg';
import pacote from 'pacote';
import path from 'path';
import semver from 'semver';
import temporary from 'tempy';
import * as PackageManager from '@expo/package-manager';
import { loginOrRegisterIfLoggedOut } from '../../accounts';
import log from '../../log';
import prompt, { Question } from '../../prompt';
import { validateGitStatusAsync } from '../utils/ProjectUtils';
type ValidationErrorMessage = string;
type DependenciesMap = { [key: string]: string | number };
export type EjectAsyncOptions = {
ejectMethod: 'bare' | 'expokit' | 'cancel';
verbose?: boolean;
force?: boolean;
packageManager?: 'npm' | 'yarn';
};
const EXPO_APP_ENTRY = 'node_modules/expo/AppEntry.js';
async function warnIfDependenciesRequireAdditionalSetupAsync(projectRoot: string): Promise<void> {
const { exp, pkg } = await ConfigUtils.readConfigJsonAsync(projectRoot);
const pkgsWithExtraSetup = await JsonFile.readAsync(
ConfigUtils.resolveModule('expo/requiresExtraSetup.json', projectRoot, exp)
);
const packagesToWarn: string[] = Object.keys(pkg.dependencies).filter(pkgName =>
pkgsWithExtraSetup.hasOwnProperty(pkgName)
);
if (packagesToWarn.length === 0) {
return;
}
let plural = packagesToWarn.length > 1;
log.nested('');
log.nested(
chalk.yellow(
`Warning: your app includes ${chalk.bold(`${packagesToWarn.length}`)} package${
plural ? 's' : ''
} that require${plural ? '' : 's'} additional setup. See the following URL${
plural ? 's' : ''
} for instructions.`
)
);
log.nested(
chalk.yellow(
`Your app may not build/run until the additional setup for ${
plural ? 'these packages' : 'this package'
} has been completed.`
)
);
log.nested('');
packagesToWarn.forEach(pkgName => {
log.nested(chalk.yellow(`- ${chalk.bold(pkgName)}: ${pkgsWithExtraSetup[pkgName]}`));
});
log.nested('');
}
export async function ejectAsync(projectRoot: string, options: EjectAsyncOptions) {
await validateGitStatusAsync();
log.nested('');
let reactNativeOptionMessage = "Bare: I'd like a bare React Native project.";
const questions: Question[] = [
{
type: 'list',
name: 'ejectMethod',
message:
'How would you like to eject your app?\n Read more: https://docs.expo.io/versions/latest/expokit/eject/',
default: 'bare',
choices: [
{
name: reactNativeOptionMessage,
value: 'bare',
short: 'Bare',
},
{
name:
"ExpoKit: I'll create or log in with an Expo account to use React Native and the Expo SDK.",
value: 'expokit',
short: 'ExpoKit',
},
{
name: "Cancel: I'll continue with my current project structure.",
value: 'cancel',
short: 'cancel',
},
],
},
];
const ejectMethod =
options.ejectMethod ||
(
await prompt(questions, {
nonInteractiveHelp:
'Please specify eject method (bare, expokit) with the --eject-method option.',
})
).ejectMethod;
if (ejectMethod === 'bare') {
await ejectToBareAsync(projectRoot);
log.nested(chalk.green('Ejected successfully!'));
log.newLine();
log.nested(
`Before running your app on iOS, make sure you have CocoaPods installed and initialize the project:`
);
log.nested('');
log.nested(` cd ios`);
log.nested(` pod install`);
log.nested('');
log.nested('Then you can run the project:');
log.nested('');
let packageManager = ConfigUtils.isUsingYarn(projectRoot) ? 'yarn' : 'npm';
log.nested(` ${packageManager === 'npm' ? 'npm run android' : 'yarn android'}`);
log.nested(` ${packageManager === 'npm' ? 'npm run ios' : 'yarn ios'}`);
await warnIfDependenciesRequireAdditionalSetupAsync(projectRoot);
} else if (ejectMethod === 'expokit') {
await loginOrRegisterIfLoggedOut();
await Detach.detachAsync(projectRoot, options);
log(chalk.green('Ejected successfully!'));
} 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.');
} else {
throw new Error(
`Unrecognized eject method "${ejectMethod}". Valid options are: bare, expokit.`
);
}
}
function ensureDependenciesMap(dependencies: any): DependenciesMap {
if (typeof dependencies !== 'object') {
throw new Error(`Dependency map is invalid, expected object but got ${typeof dependencies}`);
}
const outputMap: DependenciesMap = {};
if (!dependencies) return outputMap;
for (const key of Object.keys(dependencies)) {
const value = dependencies[key];
if (typeof value === 'string') {
outputMap[key] = value;
} else {
throw new Error(
`Dependency for key \`${key}\` should be a \`string\`, instead got: \`{ ${key}: ${JSON.stringify(
value
)} }\``
);
}
}
return outputMap;
}
async function ejectToBareAsync(projectRoot: string): Promise<void> {
const useYarn = ConfigUtils.isUsingYarn(projectRoot);
const npmOrYarn = useYarn ? 'yarn' : 'npm';
const { configPath, configName } = ConfigUtils.findConfigFile(projectRoot);
const { exp, pkg } = await ConfigUtils.readConfigJsonAsync(projectRoot);
const configBuffer = await fse.readFile(configPath);
const appJson = configName === 'app.json' ? JSON.parse(configBuffer.toString()) : {};
/**
* Perform validations
*/
if (!exp.sdkVersion) throw new Error(`Couldn't read ${configName}`);
if (!Versions.gteSdkVersion(exp, '34.0.0')) {
throw new Error(`Ejecting to a bare project is only available for SDK 34 and higher`);
}
// Validate that the template exists
let sdkMajorVersionNumber = semver.major(exp.sdkVersion);
let templateSpec = npmPackageArg(`expo-template-bare-minimum@sdk-${sdkMajorVersionNumber}`);
try {
await pacote.manifest(templateSpec);
} catch (e) {
if (e.code === 'E404') {
throw new Error(
`Unable to eject because an eject template for SDK ${sdkMajorVersionNumber} was not found`
);
} else {
throw e;
}
}
/**
* Customize app.json
*/
let { displayName, name } = await getAppNamesAsync(projectRoot);
appJson.displayName = displayName;
appJson.name = name;
if (appJson.expo.entryPoint && appJson.expo.entryPoint !== EXPO_APP_ENTRY) {
log(
chalk.yellow(`expo.entryPoint is already configured, we recommend using "${EXPO_APP_ENTRY}`)
);
} else {
appJson.expo.entryPoint = EXPO_APP_ENTRY;
}
log('Writing app.json...');
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.'));
// This is validated later...
let defaultDependencies: any = {};
let defaultDevDependencies: any = {};
/**
* Extract the template and copy it over
*/
try {
const tempDir = temporary.directory();
await Exp.extractTemplateAppAsync(templateSpec, tempDir, appJson);
fse.copySync(path.join(tempDir, 'ios'), path.join(projectRoot, 'ios'));
fse.copySync(path.join(tempDir, 'android'), path.join(projectRoot, 'android'));
const { dependencies, devDependencies } = JsonFile.read(path.join(tempDir, 'package.json'));
defaultDependencies = ensureDependenciesMap(dependencies);
defaultDevDependencies = devDependencies;
log('Successfully copied template native code.');
} catch (e) {
log(chalk.red(e.message));
log(chalk.red(`Eject failed, see above output for any issues.`));
log(chalk.yellow('You may want to delete the `ios` and/or `android` directories.'));
process.exit(1);
}
log(`Updating your package.json...`);
if (!pkg.scripts) {
pkg.scripts = {};
}
delete pkg.scripts.eject;
pkg.scripts.start = 'react-native start';
pkg.scripts.ios = 'react-native run-ios';
pkg.scripts.android = 'react-native run-android';
if (pkg.scripts.postinstall) {
pkg.scripts.postinstall = `jetify && ${pkg.scripts.postinstall}`;
log(chalk.bgYellow.black('jetifier has been added to your existing postinstall script.'));
} else {
pkg.scripts.postinstall = `jetify`;
}
// The template may have some dependencies beyond react/react-native/react-native-unimodules,
// for example RNGH and Reanimated. We should prefer the version that is already being used
// in the project for those, but swap the react/react-native/react-native-unimodules versions
// with the ones in the template.
const combinedDependencies: DependenciesMap = ensureDependenciesMap({
...defaultDependencies,
...pkg.dependencies,
});
for (const dependenciesKey of ['react', 'react-native-unimodules', 'react-native']) {
combinedDependencies[dependenciesKey] = defaultDependencies[dependenciesKey];
}
pkg.dependencies = combinedDependencies;
const combinedDevDependencies: DependenciesMap = ensureDependenciesMap({
...defaultDevDependencies,
...pkg.devDependencies,
});
combinedDevDependencies['jetifier'] = defaultDevDependencies['jetifier'];
pkg.devDependencies = combinedDevDependencies;
await fse.writeFile(path.resolve('package.json'), JSON.stringify(pkg, null, 2));
log(chalk.green('Your package.json is up to date!'));
log(`Adding entry point...`);
if (pkg.main !== EXPO_APP_ENTRY) {
log(
chalk.yellow(
`Removing "main": ${pkg.main} from package.json. We recommend using index.js instead.`
)
);
}
delete pkg.main;
await fse.writeFile(path.resolve('package.json'), JSON.stringify(pkg, null, 2));
const indexjs = `import { AppRegistry, Platform } from 'react-native';
import App from './App';
AppRegistry.registerComponent('${appJson.name}', () => App);
if (Platform.OS === 'web') {
const rootTag = document.getElementById('root') || document.getElementById('main');
AppRegistry.runApplication('${appJson.name}', { rootTag });
}
`;
await fse.writeFile(path.resolve('index.js'), indexjs);
log(chalk.green('Added new entry points!'));
log(
chalk.grey(
`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');
log('Installing new packages...');
const packageManager = PackageManager.createForProject(projectRoot, { log });
await packageManager.installAsync();
log.newLine();
}
async function getAppNamesAsync(
projectRoot: string
): Promise<{ displayName: string; name: string }> {
const { configPath, configName } = ConfigUtils.findConfigFile(projectRoot);
const { exp, pkg } = await ConfigUtils.readConfigJsonAsync(projectRoot);
const configBuffer = await fse.readFile(configPath);
const appJson = configName === 'app.json' ? JSON.parse(configBuffer.toString()) : {};
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({ length }: string): true | ValidationErrorMessage {
return length ? true : 'App display name cannot be empty.';
},
},
{
name: 'name',
message: 'What should your Android Studio and Xcode projects be called?',
default: pkg.name ? stripDashes(pkg.name) : undefined,
validate(value: string): true | ValidationErrorMessage {
if (value.length === 0) {
return 'Project name cannot be empty.';
} else if (value.includes('-') || value.includes(' ')) {
return 'Project name cannot contain hyphens or spaces.';
}
return true;
},
},
],
{
nonInteractiveHelp: 'Please specify "displayName" and "name" in app.json.',
}
));
}
return { displayName, name };
}
function stripDashes(s: string): string {
let ret = '';
for (let c of s) {
if (c !== ' ' && c !== '-') {
ret += c;
}
}
return ret;
}