Skip to content

Commit

Permalink
CLI: Add support for templates fetched from npm
Browse files Browse the repository at this point in the history
Summary:
This PR allows anyone to publish templates for React Native.

It's possible for people to publish modules for React Native, we should also support custom templates. A suggestion from a Cordova mantainer where they did the same thing suggests this is useful:
https://github.com/mkonicek/AppTemplateFeedback/issues/1

I published a sample template [react-native-template-demo](https://www.npmjs.com/package/react-native-template-demo).

(GitHub: https://github.com/mkonicek/react-native-template-demo)

With this PR anyone can then use that template:

`react-native init MyApp --template demo`

The convention is: if someone publishes an npm package called `react-native-template-foo`, people can use it by running `react-native init MyApp --template foo`.

Use a template called `react-native-template-demo` from npm:

`react-native init MyApp --template demo`

Use a local template:

`react-native init MyApp --template file:///path_to/react-native-template-dem
Closes #12548

Differential Revision: D4620567

Pulled By: mkonicek

fbshipit-source-id: bb40d457a7fec28edb577f08137e73241072de3a
  • Loading branch information
Martin Konicek authored and facebook-github-bot committed Feb 27, 2017
1 parent 37b91a6 commit 17c175a
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 49 deletions.
20 changes: 20 additions & 0 deletions local-cli/generator/copyProjectTemplateAndReplace.js
Expand Up @@ -21,6 +21,12 @@ const walk = require('../util/walk');
* @param srcPath e.g. '/Users/martin/AwesomeApp/node_modules/react-native/local-cli/templates/HelloWorld'
* @param destPath e.g. '/Users/martin/AwesomeApp'
* @param newProjectName e.g. 'AwesomeApp'
* @param options e.g. {
* upgrade: true,
* force: false,
* displayName: 'Hello World',
* ignorePaths: ['template/file/to/ignore.md'],
* }
*/
function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, options) {
if (!srcPath) { throw new Error('Need a path to copy from'); }
Expand All @@ -45,6 +51,20 @@ function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, option
.replace(/HelloWorld/g, newProjectName)
.replace(/helloworld/g, newProjectName.toLowerCase());

// Templates may contain files that we don't want to copy.
// Examples:
// - Dummy package.json file included in the template only for publishing to npm
// - Docs specific to the template (.md files)
if (options.ignorePaths) {
if (!Array.isArray(options.ignorePaths)) {
throw new Error('options.ignorePaths must be an array');
}
if (options.ignorePaths.some(ignorePath => ignorePath === relativeFilePath)) {
// Skip copying this file
return;
}
}

let contentChangedCallback = null;
if (options.upgrade && (!options.force)) {
contentChangedCallback = (_, contentChanged) => {
Expand Down
176 changes: 134 additions & 42 deletions local-cli/generator/templates.js
Expand Up @@ -13,7 +13,10 @@ const execSync = require('child_process').execSync;
const fs = require('fs');
const path = require('path');

const availableTemplates = {
/**
* Templates released as part of react-native in local-cli/templates.
*/
const builtInTemplates = {
navigation: 'HelloNavigation',
};

Expand All @@ -22,9 +25,9 @@ function listTemplatesAndExit(newProjectName, options) {
// Just listing templates using 'react-native init --template'.
// Not creating a new app.
// Print available templates and exit.
const templateKeys = Object.keys(availableTemplates);
const templateKeys = Object.keys(builtInTemplates);
if (templateKeys.length === 0) {
// Just a guard, should never happen as long availableTemplates
// Just a guard, should never happen as long builtInTemplates
// above is defined correctly :)
console.log(
'There are no templates available besides ' +
Expand All @@ -47,63 +50,152 @@ function listTemplatesAndExit(newProjectName, options) {
}

/**
* @param destPath Create the new project at this path.
* @param newProjectName For example 'AwesomeApp'.
* @param templateKey Template to use, for example 'navigation'.
* @param template 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) {
function createProjectFromTemplate(destPath, newProjectName, template, 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
);
if (template === undefined) {
// No specific template, use just the HelloWorld template above
return;
}

// 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 builtInTemplateName = builtInTemplates[template];
if (builtInTemplateName) {
// template is e.g. 'navigation',
// use the built-in local-cli/templates/HelloNavigation folder
createFromBuiltInTemplate(builtInTemplateName, destPath, newProjectName, yarnVersion);
} else {
// template is e.g. 'ignite',
// use the template react-native-template-ignite from npm
createFromRemoteTemplate(template, destPath, newProjectName, yarnVersion);
}
}

// (We might want to get rid of built-in templates in the future -
// publish them to npm and install from there.)
function createFromBuiltInTemplate(templateName, destPath, newProjectName, yarnVersion) {
const templatePath = path.resolve(
'node_modules', 'react-native', 'local-cli', 'templates', templateName
);
copyProjectTemplateAndReplace(
templatePath,
destPath,
newProjectName,
);
installTemplateDependencies(templatePath, yarnVersion);
}

/**
* The following formats are supported for the template:
* - 'demo' -> Fetch the package react-native-template-demo from npm
* - git://..., http://..., file://... or any other URL supported by npm
*/
function createFromRemoteTemplate(template, destPath, newProjectName, yarnVersion) {
let installPackage;
let templateName;
if (template.includes('://')) {
// URL, e.g. git://, file://
installPackage = template;
templateName = template.substr(template.lastIndexOf('/') + 1);
} else {
// e.g 'demo'
installPackage = 'react-native-template-' + template;
templateName = installPackage;
}

// Check if the template exists
console.log(`Fetching template ${installPackage}...`);
try {
if (yarnVersion) {
execSync(`yarn add ${installPackage} --ignore-scripts`, {stdio: 'inherit'});
} else {
throw new Error('Uknown template: ' + templateKey);
execSync(`npm install ${installPackage} --save --save-exact --ignore-scripts`, {stdio: 'inherit'});
}
const templatePath = path.resolve(
'node_modules', templateName
);
copyProjectTemplateAndReplace(
templatePath,
destPath,
newProjectName,
{
// Every template contains a dummy package.json file included
// only for publishing the template to npm.
// We want to ignore this dummy file, otherwise it would overwrite
// our project's package.json file.
ignorePaths: ['package.json', 'dependencies.json'],
}
);
installTemplateDependencies(templatePath, yarnVersion);
} finally {
// Clean up the temp files
try {
if (yarnVersion) {
execSync(`yarn remove ${templateName} --ignore-scripts`);
} else {
execSync(`npm uninstall ${templateName} --ignore-scripts`);
}
} catch (err) {
// Not critical but we still want people to know and report
// if this the clean up fails.
console.warn(
`Failed to clean up template temp files in node_modules/${templateName}. ` +
'This is not a critical error, you can work on your app.'
);
}
}
}

// Add dependencies:
function installTemplateDependencies(templatePath, yarnVersion) {
// dependencies.json is a special file that lists additional dependencies
// that are required by this template
const dependenciesJsonPath = path.resolve(
templatePath, 'dependencies.json'
);
console.log('Adding dependencies for the project...');
if (!fs.existsSync(dependenciesJsonPath)) {
console.log('No additional dependencies.');
return;
}

// 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'
let dependencies;
try {
dependencies = JSON.parse(fs.readFileSync(dependenciesJsonPath));
} catch (err) {
throw new Error(
'Could not parse the template\'s dependencies.json: ' + err.message
);
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'});
}
}
}
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'});
}
}
console.log('Linking native dependencies into the project\'s build files...');
execSync('react-native link', {stdio: 'inherit'});
}

module.exports = {
Expand Down
20 changes: 13 additions & 7 deletions local-cli/link/link.js
Expand Up @@ -5,6 +5,8 @@
* 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.
*
* @flow
*/

const log = require('npmlog');
Expand All @@ -29,6 +31,8 @@ const pollParams = require('./pollParams');
const commandStub = require('./commandStub');
const promisify = require('./promisify');

import type {ConfigT} from '../core';

log.heading = 'rnpm-link';

const dedupeAssets = (assets) => uniq(assets, asset => path.basename(asset));
Expand Down Expand Up @@ -125,18 +129,20 @@ const linkAssets = (project, assets) => {
};

/**
* Updates project and links all dependencies to it
* Updates project and links all dependencies to it.
*
* If optional argument [packageName] is provided, it's the only one that's checked
* @param args If optional argument [packageName] is provided,
* only that package is processed.
* @param config CLI config, see local-cli/core/index.js
*/
function link(args, config) {
function link(args: Array<string>, config: ConfigT) {
var project;
try {
project = config.getProjectConfig();
} catch (err) {
log.error(
'ERRPACKAGEJSON',
'No package found. Are you sure it\'s a React Native project?'
'No package found. Are you sure this is a React Native project?'
);
return Promise.reject(err);
}
Expand Down Expand Up @@ -169,15 +175,15 @@ function link(args, config) {

return promiseWaterfall(tasks).catch(err => {
log.error(
`It seems something went wrong while linking. Error: ${err.message} \n`
+ 'Please file an issue here: https://github.com/facebook/react-native/issues'
`Something went wrong while linking. Error: ${err.message} \n` +
'Please file an issue here: https://github.com/facebook/react-native/issues'
);
throw err;
});
}

module.exports = {
func: link,
description: 'links all native dependencies',
description: 'links all native dependencies (updates native build files)',
name: 'link [packageName]',
};

0 comments on commit 17c175a

Please sign in to comment.