From a54d449e947afc609a404a699494902823fbbf5e Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Mon, 6 Feb 2017 12:21:13 -0800 Subject: [PATCH] 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: screenshot 2017-02-02 16 56 28 screenshot 2017-02-02 16 58 04 - 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 --- local-cli/generator/templates.js | 112 ++++++++++++++++++ local-cli/init/init.js | 26 ++-- local-cli/runAndroid/runAndroid.js | 2 +- .../HelloNavigation/dependencies.json | 3 + .../HelloNavigation/index.android.js | 2 +- .../templates/HelloNavigation/index.ios.js | 2 +- .../views/HomeScreenTabNavigator.js | 8 +- .../views/chat/ChatListScreen.js | 2 +- .../WelcomeScreen.js} | 11 +- .../views/welcome/WelcomeText.android.js | 51 ++++++++ .../views/welcome/WelcomeText.ios.js | 51 ++++++++ .../welcome-icon.png} | Bin react-native-cli/index.js | 7 +- 13 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 local-cli/generator/templates.js create mode 100644 local-cli/templates/HelloNavigation/dependencies.json rename local-cli/templates/HelloNavigation/views/{friends/FriendListScreen.js => welcome/WelcomeScreen.js} (75%) create mode 100644 local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js create mode 100644 local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js rename local-cli/templates/HelloNavigation/views/{friends/friend-icon.png => welcome/welcome-icon.png} (100%) diff --git a/local-cli/generator/templates.js b/local-cli/generator/templates.js new file mode 100644 index 00000000000000..dfbf598e2558b7 --- /dev/null +++ b/local-cli/generator/templates.js @@ -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, +}; diff --git a/local-cli/init/init.js b/local-cli/init/init.js index 15f9837dfe8b8e..3537a97ba68292 100644 --- a/local-cli/init/init.js +++ b/local-cli/init/init.js @@ -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,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); + } } /** @@ -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...'); diff --git a/local-cli/runAndroid/runAndroid.js b/local-cli/runAndroid/runAndroid.js index 2adf2386e0b9c9..815c8ebaa746fc 100644 --- a/local-cli/runAndroid/runAndroid.js +++ b/local-cli/runAndroid/runAndroid.js @@ -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, { diff --git a/local-cli/templates/HelloNavigation/dependencies.json b/local-cli/templates/HelloNavigation/dependencies.json new file mode 100644 index 00000000000000..130c08ed81f3c6 --- /dev/null +++ b/local-cli/templates/HelloNavigation/dependencies.json @@ -0,0 +1,3 @@ +{ + "react-navigation": "1.0.0-beta.1" +} diff --git a/local-cli/templates/HelloNavigation/index.android.js b/local-cli/templates/HelloNavigation/index.android.js index b122bdc5dcf0d9..e9ea66bf64ba48 100644 --- a/local-cli/templates/HelloNavigation/index.android.js +++ b/local-cli/templates/HelloNavigation/index.android.js @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native'; import MainNavigator from './views/MainNavigator'; -AppRegistry.registerComponent('ChatExample', () => MainNavigator); +AppRegistry.registerComponent('HelloWorld', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/index.ios.js b/local-cli/templates/HelloNavigation/index.ios.js index b122bdc5dcf0d9..e9ea66bf64ba48 100644 --- a/local-cli/templates/HelloNavigation/index.ios.js +++ b/local-cli/templates/HelloNavigation/index.ios.js @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native'; import MainNavigator from './views/MainNavigator'; -AppRegistry.registerComponent('ChatExample', () => MainNavigator); +AppRegistry.registerComponent('HelloWorld', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js index 123ee3a78a1738..bcbfaaa5de76f1 100644 --- a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js +++ b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js @@ -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; diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js index a29fa1443af90c..146e7e2143101a 100644 --- a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js +++ b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js @@ -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', }, diff --git a/local-cli/templates/HelloNavigation/views/friends/FriendListScreen.js b/local-cli/templates/HelloNavigation/views/welcome/WelcomeScreen.js similarity index 75% rename from local-cli/templates/HelloNavigation/views/friends/FriendListScreen.js rename to local-cli/templates/HelloNavigation/views/welcome/WelcomeScreen.js index 85d8cdd79494c1..d149714ff73e07 100644 --- a/local-cli/templates/HelloNavigation/views/friends/FriendListScreen.js +++ b/local-cli/templates/HelloNavigation/views/welcome/WelcomeScreen.js @@ -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 }) => ( ), @@ -29,9 +30,7 @@ export default class FriendListScreen extends Component { render() { return ( - - A list of friends here. - + ); } } diff --git a/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js new file mode 100644 index 00000000000000..a082661a2ded76 --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js @@ -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 ( + + + Welcome to React Native! + + + This app shows the basics of navigating between a few screens, + working with ListView and handling text input. + + + Modify any files to get started. For example try changing the + file views/welcome/WelcomeText.android.js. + + + Press Cmd+R to reload,{'\n'} + Cmd+D or shake for dev menu. + + + ); + } +} + +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, + }, +}); diff --git a/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js new file mode 100644 index 00000000000000..9d5ce075fad412 --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js @@ -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 ( + + + Welcome to React Native! + + + This app shows the basics of navigating between a few screens, + working with ListView and handling text input. + + + Modify any files to get started. For example try changing the + file{'\n'}views/welcome/WelcomeText.ios.js. + + + Press Cmd+R to reload,{'\n'} + Cmd+D or shake for dev menu. + + + ); + } +} + +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, + }, +}); diff --git a/local-cli/templates/HelloNavigation/views/friends/friend-icon.png b/local-cli/templates/HelloNavigation/views/welcome/welcome-icon.png similarity index 100% rename from local-cli/templates/HelloNavigation/views/friends/friend-icon.png rename to local-cli/templates/HelloNavigation/views/welcome/welcome-icon.png diff --git a/react-native-cli/index.js b/react-native-cli/index.js index 95355f835a63dc..795e8143edb83d 100755 --- a/react-native-cli/index.js +++ b/react-native-cli/index.js @@ -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 - 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;