Eject CLI command to re-create native folders

Summary:
The iOS and Android native folders of a React Native app can be difficult to maintain. This introduces a new workflow for creating and maintaining the native code of your app.

Now it will be possible to:

1. Remove the native iOS or Android folders
2. Create an `app.json` for your app, with at least a `name` and `displayName`
3. Run `react-native eject`, and the native code for your app will be generated

Then, as usual, you can run `react-native run-ios` and `react-native run-android`, to build and launch your app

For apps that don't have any native code, it will be possible to ignore the `ios` and `android` folders from version control.

Eject step tested in RN app by deleting native folders.

mkonicek, what is the best way to test `react-native init`?

As follow-up items, we can enable the following:

- Configuring app icon and launch screen from the `app.json`
- Automatically run `react-native link` for native libraries
- A
Closes https://github.com/facebook/react-native/pull/12162

Differential Revision: D4509138

Pulled By: ericvicenti

fbshipit-source-id: 0ee213e68f0a3d44bfce337e3ec43e5024bacc66
This commit is contained in:
Eric Vicenti 2017-02-03 12:52:27 -08:00 committed by Facebook Github Bot
parent 0a71f48b13
commit bc74dd1e14
6 changed files with 109 additions and 3 deletions

View File

@ -42,6 +42,7 @@ const documentedCommands = [
require('./library/library'),
require('./bundle/bundle'),
require('./bundle/unbundle'),
require('./eject/eject'),
require('./link/link'),
require('./link/unlink'),
require('./install/install'),

96
local-cli/eject/eject.js Normal file
View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const copyProjectTemplateAndReplace = require('../generator/copyProjectTemplateAndReplace');
const path = require('path');
const fs = require('fs');
/**
* The eject command re-creates the `android` and `ios` native folders. Because native code can be
* difficult to maintain, this new script allows an `app.json` to be defined for the project, which
* is used to configure the native app.
*
* The `app.json` config may contain the following keys:
*
* - `name` - The short name used for the project, should be TitleCase
* - `displayName` - The app's name on the home screen
*/
function eject() {
const doesIOSExist = fs.existsSync(path.resolve('ios'));
const doesAndroidExist = fs.existsSync(path.resolve('android'));
if (doesIOSExist && doesAndroidExist) {
console.error(
'Both the iOS and Android folders already exist! Please delete `ios` and/or `android` ' +
'before ejecting.'
);
process.exit(1);
}
let appConfig = null;
try {
appConfig = require(path.resolve('app.json'));
} catch(e) {
console.error(
`Eject requires an \`app.json\` config file to be located at ` +
`${path.resolve('app.json')}, and it must at least specify a \`name\` for the project ` +
`name, and a \`displayName\` for the app's home screen label.`
);
process.exit(1);
}
const appName = appConfig.name;
if (!appName) {
console.error(
`App \`name\` must be defined in the \`app.json\` config file to define the project name. `+
`It must not contain any spaces or dashes.`
);
process.exit(1);
}
const displayName = appConfig.displayName;
if (!displayName) {
console.error(
`App \`displayName\` must be defined in the \`app.json\` config file, to define the label ` +
`of the app on the home screen.`
);
process.exit(1);
}
const templateOptions = { displayName };
if (!doesIOSExist) {
console.log('Generating the iOS folder.');
copyProjectTemplateAndReplace(
path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld', 'ios'),
path.resolve('ios'),
appName,
templateOptions
);
}
if (!doesAndroidExist) {
console.log('Generating the Android folder.');
copyProjectTemplateAndReplace(
path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld', 'android'),
path.resolve('android'),
appName,
templateOptions
);
}
}
module.exports = {
name: 'eject',
description: 'Re-create the iOS and Android folders and native code',
func: eject,
options: [],
};

View File

@ -27,10 +27,12 @@ function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, option
if (!destPath) { throw new Error('Need a path to copy to'); }
if (!newProjectName) { throw new Error('Need a project name'); }
options = options || {};
walk(srcPath).forEach(absoluteSrcFilePath => {
// 'react-native upgrade'
if (options && options.upgrade) {
if (options.upgrade) {
// Don't upgrade these files
const fileName = path.basename(absoluteSrcFilePath);
// This also includes __tests__/index.*.js
@ -44,7 +46,7 @@ function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, option
.replace(/helloworld/g, newProjectName.toLowerCase());
let contentChangedCallback = null;
if (options && options.upgrade && (!options.force)) {
if (options.upgrade && (!options.force)) {
contentChangedCallback = (_, contentChanged) => {
return upgradeFileContentChangedCallback(
absoluteSrcFilePath,
@ -57,6 +59,7 @@ function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, option
absoluteSrcFilePath,
path.resolve(destPath, relativeRenamedPath),
{
'Hello App Display Name': options.displayName || newProjectName,
'HelloWorld': newProjectName,
'helloworld': newProjectName.toLowerCase(),
},

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">HelloWorld</string>
<string name="app_name">Hello App Display Name</string>
</resources>

View File

@ -0,0 +1,4 @@
{
"name": "HelloWorld",
"displayName": "HelloWorld"
}

View File

@ -4,6 +4,8 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Hello App Display Name</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>