Skip to content

Commit

Permalink
CLI: Add support for project templates
Browse files Browse the repository at this point in the history
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 #12170

Differential Revision: D4516241

Pulled By: mkonicek

fbshipit-source-id: 8ac081157919872e92947ed64ea64fb48078614d
  • Loading branch information
Martin Konicek committed Feb 8, 2017
1 parent ff8257c commit 3a6dff4
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 27 deletions.
112 changes: 112 additions & 0 deletions local-cli/generator/templates.js
Original file line number Diff line number Diff line change
@@ -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,
};
26 changes: 16 additions & 10 deletions local-cli/init/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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;
Expand All @@ -40,7 +43,14 @@ function init(projectDir, argsOrName) {
const newProjectName = args[0];
const options = minimist(args);

generateProject(projectDir, newProjectName, options);
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);
}
}

/**
Expand All @@ -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...');
Expand Down
2 changes: 1 addition & 1 deletion local-cli/runAndroid/runAndroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
3 changes: 3 additions & 0 deletions local-cli/templates/HelloNavigation/dependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"react-navigation": "1.0.0-beta.1"
}
2 changes: 1 addition & 1 deletion local-cli/templates/HelloNavigation/index.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native';

import MainNavigator from './views/MainNavigator';

AppRegistry.registerComponent('ChatExample', () => MainNavigator);
AppRegistry.registerComponent('HelloWorld', () => MainNavigator);
2 changes: 1 addition & 1 deletion local-cli/templates/HelloNavigation/index.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native';

import MainNavigator from './views/MainNavigator';

AppRegistry.registerComponent('ChatExample', () => MainNavigator);
AppRegistry.registerComponent('HelloWorld', () => MainNavigator);
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ 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',
},
tabBar: {
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}]}
/>
),
Expand All @@ -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 />
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
},
});
Original file line number Diff line number Diff line change
@@ -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,
},
});
7 changes: 4 additions & 3 deletions react-native-cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 3a6dff4

Please sign in to comment.