diff --git a/local-cli/commands.js b/local-cli/commands.js
index 5f2fe27bb42e26..534a59f364531d 100644
--- a/local-cli/commands.js
+++ b/local-cli/commands.js
@@ -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'),
diff --git a/local-cli/eject/eject.js b/local-cli/eject/eject.js
new file mode 100644
index 00000000000000..b18a5176ec1c93
--- /dev/null
+++ b/local-cli/eject/eject.js
@@ -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: [],
+};
diff --git a/local-cli/generator/copyProjectTemplateAndReplace.js b/local-cli/generator/copyProjectTemplateAndReplace.js
index 7818022715ebe2..c24a3d3b0ea3cf 100644
--- a/local-cli/generator/copyProjectTemplateAndReplace.js
+++ b/local-cli/generator/copyProjectTemplateAndReplace.js
@@ -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(),
},
diff --git a/local-cli/templates/HelloWorld/android/app/src/main/res/values/strings.xml b/local-cli/templates/HelloWorld/android/app/src/main/res/values/strings.xml
index b89cc148915cb8..0c79c4bad47cb7 100644
--- a/local-cli/templates/HelloWorld/android/app/src/main/res/values/strings.xml
+++ b/local-cli/templates/HelloWorld/android/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- HelloWorld
+ Hello App Display Name
diff --git a/local-cli/templates/HelloWorld/app.json b/local-cli/templates/HelloWorld/app.json
new file mode 100644
index 00000000000000..114d929c1581c5
--- /dev/null
+++ b/local-cli/templates/HelloWorld/app.json
@@ -0,0 +1,4 @@
+{
+ "name": "HelloWorld",
+ "displayName": "HelloWorld"
+}
\ No newline at end of file
diff --git a/local-cli/templates/HelloWorld/ios/HelloWorld/Info.plist b/local-cli/templates/HelloWorld/ios/HelloWorld/Info.plist
index 2fb6a11c2c3356..a58201cfa82b98 100644
--- a/local-cli/templates/HelloWorld/ios/HelloWorld/Info.plist
+++ b/local-cli/templates/HelloWorld/ios/HelloWorld/Info.plist
@@ -4,6 +4,8 @@
CFBundleDevelopmentRegion
en
+ CFBundleDisplayName
+ Hello App Display Name
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier