CLI: Add support for project templates

Summary:
Currently it is not trivial for people to get started with React Native. `react-native init MyApp` just creates a simple app with a single screen. People have to spend time figuring out how to add more screens, or how to accomplish very basic tasks such as rendering a list of data or handling text input.

Let's add an option: `react-native init --template navigation` - this creates a "starter" app which can be easily tweaked into the actual app the person wants to build.

**Test plan (required)**

- Checked that 'react-native init MyApp' still works as before:

<img width="487" alt="screenshot 2017-02-02 16 56 28" src="https://cloud.githubusercontent.com/assets/346214/22559344/b2348ebe-e968-11e6-9032-d1c33216f490.png">

<img width="603" alt="screenshot 2017-02-02 16 58 04" src="https://cloud.githubusercontent.com/assets/346214/22559370/c96a2ca6-e968-11e6-91f7-7afb967920fc.png">

- Ran 'react-native init MyNavApp --template'. This prints the available templates:

```
$ react-native init MyNavApp
Closes https://github.com/facebook/react-native/pull/12170

Differential Revision: D4516241

Pulled By: mkonicek

fbshipit-source-id: 8ac081157919872e92947ed64ea64fb48078614d
This commit is contained in:
Martin Konicek 2017-02-06 12:21:13 -08:00 committed by Facebook Github Bot
parent 75c14e3674
commit a54d449e94
13 changed files with 250 additions and 27 deletions

View File

@ -0,0 +1,112 @@
/**
* 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('./copyProjectTemplateAndReplace');
const execSync = require('child_process').execSync;
const fs = require('fs');
const path = require('path');
const availableTemplates = {
navigation: 'HelloNavigation',
};
function listTemplatesAndExit(newProjectName, options) {
if (options.template === true) {
// Just listing templates using 'react-native init --template'.
// Not creating a new app.
// Print available templates and exit.
const templateKeys = Object.keys(availableTemplates);
if (templateKeys.length === 0) {
// Just a guard, should never happen as long availableTemplates
// above is defined correctly :)
console.log(
'There are no templates available besides ' +
'the default "Hello World" one.'
);
} else {
console.log(
'The available templates are:\n' +
templateKeys.join('\n') +
'\nYou can use these to create an app based on a template, for example: ' +
'you could run: ' +
'react-native init ' + newProjectName + ' --template ' + templateKeys[0]
);
}
// Exit 'react-native init'
return true;
}
// Continue 'react-native init'
return false;
}
/**
* @param newProjectName For example 'AwesomeApp'.
* @param templateKey Template to use, for example 'navigation'.
* @param yarnVersion Version of yarn available on the system, or null if
* yarn is not available. For example '0.18.1'.
*/
function createProjectFromTemplate(destPath, newProjectName, templateKey, yarnVersion) {
// Expand the basic 'HelloWorld' template
copyProjectTemplateAndReplace(
path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'),
destPath,
newProjectName
);
if (templateKey !== undefined) {
// Keep the files from the 'HelloWorld' template, and overwrite some of them
// with the specified project template.
// The 'HelloWorld' template contains the native files (these are used by
// all templates) and every other template only contains additional JS code.
// Reason:
// This way we don't have to duplicate the native files in every template.
// If we duplicated them we'd make RN larger and risk that people would
// forget to maintain all the copies so they would go out of sync.
const templateName = availableTemplates[templateKey];
if (templateName) {
copyProjectTemplateAndReplace(
path.resolve(
'node_modules', 'react-native', 'local-cli', 'templates', templateName
),
destPath,
newProjectName
);
} else {
throw new Error('Uknown template: ' + templateKey);
}
// Add dependencies:
// dependencies.json is a special file that lists additional dependencies
// that are required by this template
const dependenciesJsonPath = path.resolve(
'node_modules', 'react-native', 'local-cli', 'templates', templateName, 'dependencies.json'
);
if (fs.existsSync(dependenciesJsonPath)) {
console.log('Adding dependencies for the project...');
const dependencies = JSON.parse(fs.readFileSync(dependenciesJsonPath));
for (let depName in dependencies) {
const depVersion = dependencies[depName];
const depToInstall = depName + '@' + depVersion;
console.log('Adding ' + depToInstall + '...');
if (yarnVersion) {
execSync(`yarn add ${depToInstall}`, {stdio: 'inherit'});
} else {
execSync(`npm install ${depToInstall} --save --save-exact`, {stdio: 'inherit'});
}
}
}
}
}
module.exports = {
listTemplatesAndExit,
createProjectFromTemplate,
};

View File

@ -8,7 +8,10 @@
*/
'use strict';
const copyProjectTemplateAndReplace = require('../generator/copyProjectTemplateAndReplace');
const {
listTemplatesAndExit,
createProjectFromTemplate,
} = require('../generator/templates');
const execSync = require('child_process').execSync;
const fs = require('fs');
const minimist = require('minimist');
@ -23,15 +26,15 @@ const yarn = require('../util/yarn');
* @param projectDir Templates will be copied here.
* @param argsOrName Project name or full list of custom arguments
* for the generator.
* @param options Command line options passed from the react-native-cli directly.
* E.g. `{ version: '0.43.0', template: 'navigation' }`
*/
function init(projectDir, argsOrName) {
console.log('Setting up new React Native app in ' + projectDir);
const args = Array.isArray(argsOrName)
? argsOrName // argsOrName was e.g. ['AwesomeApp', '--verbose']
: [argsOrName].concat(process.argv.slice(4)); // argsOrName was e.g. 'AwesomeApp'
// args array is e.g. ['AwesomeApp', '--verbose']
// args array is e.g. ['AwesomeApp', '--verbose', '--template', 'navigation']
if (!args || args.length === 0) {
console.error('react-native init requires a project name.');
return;
@ -40,8 +43,15 @@ function init(projectDir, argsOrName) {
const newProjectName = args[0];
const options = minimist(args);
if (listTemplatesAndExit(newProjectName, options)) {
// Just listing templates using 'react-native init --template'
// Not creating a new app.
return;
} else {
console.log('Setting up new React Native app in ' + projectDir);
generateProject(projectDir, newProjectName, options);
}
}
/**
* Generates a new React Native project based on the template.
@ -67,11 +77,7 @@ function generateProject(destinationRoot, newProjectName, options) {
yarn.getYarnVersionIfAvailable() &&
yarn.isGlobalCliUsingYarn(destinationRoot);
copyProjectTemplateAndReplace(
path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'),
destinationRoot,
newProjectName
);
createProjectFromTemplate(destinationRoot, newProjectName, options.template, yarnVersion);
if (yarnVersion) {
console.log('Adding React...');

View File

@ -193,7 +193,7 @@ function runOnAllDevices(args, cmd, packageName, adbPath){
}
console.log(chalk.bold(
`Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join(' ')}...`
`Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join(' ')})...`
));
child_process.execFileSync(cmd, gradleArgs, {

View File

@ -0,0 +1,3 @@
{
"react-navigation": "1.0.0-beta.1"
}

View File

@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native';
import MainNavigator from './views/MainNavigator';
AppRegistry.registerComponent('ChatExample', () => MainNavigator);
AppRegistry.registerComponent('HelloWorld', () => MainNavigator);

View File

@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native';
import MainNavigator from './views/MainNavigator';
AppRegistry.registerComponent('ChatExample', () => MainNavigator);
AppRegistry.registerComponent('HelloWorld', () => MainNavigator);

View File

@ -7,18 +7,18 @@ import {
import { TabNavigator } from 'react-navigation';
import ChatListScreen from './chat/ChatListScreen';
import FriendListScreen from './friends/FriendListScreen';
import WelcomeScreen from './welcome/WelcomeScreen';
/**
* Screen with tabs shown on app startup.
*/
const HomeScreenTabNavigator = TabNavigator({
Welcome: {
screen: WelcomeScreen,
},
Chats: {
screen: ChatListScreen,
},
Friends: {
screen: FriendListScreen,
},
});
export default HomeScreenTabNavigator;

View File

@ -10,7 +10,7 @@ import ListItem from '../../components/ListItem';
export default class ChatListScreen extends Component {
static navigationOptions = {
title: 'Chats',
title: 'Friends',
header: {
visible: Platform.OS === 'ios',
},

View File

@ -8,11 +8,12 @@ import {
} from 'react-native';
import ListItem from '../../components/ListItem';
import WelcomeText from './WelcomeText';
export default class FriendListScreen extends Component {
export default class WelcomeScreen extends Component {
static navigationOptions = {
title: 'Friends',
title: 'Welcome',
header: {
visible: Platform.OS === 'ios',
},
@ -20,7 +21,7 @@ export default class FriendListScreen extends Component {
icon: ({ tintColor }) => (
<Image
// Using react-native-vector-icons works here too
source={require('./friend-icon.png')}
source={require('./welcome-icon.png')}
style={[styles.icon, {tintColor: tintColor}]}
/>
),
@ -29,9 +30,7 @@ export default class FriendListScreen extends Component {
render() {
return (
<View style={styles.container}>
<Text>A list of friends here.</Text>
</View>
<WelcomeText />
);
}
}

View File

@ -0,0 +1,51 @@
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
export default class WelcomeText extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
This app shows the basics of navigating between a few screens,
working with ListView and handling text input.
</Text>
<Text style={styles.instructions}>
Modify any files to get started. For example try changing the
file views/welcome/WelcomeText.android.js.
</Text>
<Text style={styles.instructions}>
Press Cmd+R to reload,{'\n'}
Cmd+D or shake for dev menu.
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
padding: 20,
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 16,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 12,
},
});

View File

@ -0,0 +1,51 @@
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
export default class WelcomeText extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
This app shows the basics of navigating between a few screens,
working with ListView and handling text input.
</Text>
<Text style={styles.instructions}>
Modify any files to get started. For example try changing the
file{'\n'}views/welcome/WelcomeText.ios.js.
</Text>
<Text style={styles.instructions}>
Press Cmd+R to reload,{'\n'}
Cmd+D or shake for dev menu.
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
padding: 20,
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 16,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 12,
},
});

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -48,6 +48,7 @@ var semver = require('semver');
* if you are in a RN app folder
* init - to create a new project and npm install it
* --verbose - to print logs while init
* --template - name of the template to use, e.g. --template navigation
* --version <alternative react-native package> - override default (https://registry.npmjs.org/react-native@latest),
* package to install, examples:
* - "0.22.0-rc1" - A new app will be created using a specific version of React Native from npm repo
@ -129,7 +130,8 @@ if (cli) {
' Options:',
'',
' -h, --help output usage information',
' -v, --version output the version number',
' -v, --version use a specific version of React Native',
' --template use an app template. Use --template to see available templates.',
'',
].join('\n'));
process.exit(0);
@ -264,8 +266,7 @@ function getInstallPackage(rnPackage) {
}
function run(root, projectName, options) {
// E.g. '0.38' or '/path/to/archive.tgz'
const rnPackage = options.version;
const rnPackage = options.version; // e.g. '0.38' or '/path/to/archive.tgz'
const forceNpmClient = options.npm;
const yarnVersion = (!forceNpmClient) && getYarnVersionIfAvailable();
var installCommand;