CLI: Add support for templates fetched from npm
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 https://github.com/facebook/react-native/pull/12548 Differential Revision: D4620567 Pulled By: mkonicek fbshipit-source-id: bb40d457a7fec28edb577f08137e73241072de3a
This commit is contained in:
parent
37b91a63c7
commit
17c175a149
|
@ -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 srcPath e.g. '/Users/martin/AwesomeApp/node_modules/react-native/local-cli/templates/HelloWorld'
|
||||||
* @param destPath e.g. '/Users/martin/AwesomeApp'
|
* @param destPath e.g. '/Users/martin/AwesomeApp'
|
||||||
* @param newProjectName e.g. '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) {
|
function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, options) {
|
||||||
if (!srcPath) { throw new Error('Need a path to copy from'); }
|
if (!srcPath) { throw new Error('Need a path to copy from'); }
|
||||||
|
@ -45,6 +51,20 @@ function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, option
|
||||||
.replace(/HelloWorld/g, newProjectName)
|
.replace(/HelloWorld/g, newProjectName)
|
||||||
.replace(/helloworld/g, newProjectName.toLowerCase());
|
.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;
|
let contentChangedCallback = null;
|
||||||
if (options.upgrade && (!options.force)) {
|
if (options.upgrade && (!options.force)) {
|
||||||
contentChangedCallback = (_, contentChanged) => {
|
contentChangedCallback = (_, contentChanged) => {
|
||||||
|
|
|
@ -13,7 +13,10 @@ const execSync = require('child_process').execSync;
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const availableTemplates = {
|
/**
|
||||||
|
* Templates released as part of react-native in local-cli/templates.
|
||||||
|
*/
|
||||||
|
const builtInTemplates = {
|
||||||
navigation: 'HelloNavigation',
|
navigation: 'HelloNavigation',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,9 +25,9 @@ function listTemplatesAndExit(newProjectName, options) {
|
||||||
// Just listing templates using 'react-native init --template'.
|
// Just listing templates using 'react-native init --template'.
|
||||||
// Not creating a new app.
|
// Not creating a new app.
|
||||||
// Print available templates and exit.
|
// Print available templates and exit.
|
||||||
const templateKeys = Object.keys(availableTemplates);
|
const templateKeys = Object.keys(builtInTemplates);
|
||||||
if (templateKeys.length === 0) {
|
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 :)
|
// above is defined correctly :)
|
||||||
console.log(
|
console.log(
|
||||||
'There are no templates available besides ' +
|
'There are no templates available besides ' +
|
||||||
|
@ -47,12 +50,13 @@ function listTemplatesAndExit(newProjectName, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param destPath Create the new project at this path.
|
||||||
* @param newProjectName For example 'AwesomeApp'.
|
* @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
|
* @param yarnVersion Version of yarn available on the system, or null if
|
||||||
* yarn is not available. For example '0.18.1'.
|
* 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
|
// Expand the basic 'HelloWorld' template
|
||||||
copyProjectTemplateAndReplace(
|
copyProjectTemplateAndReplace(
|
||||||
path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'),
|
path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'),
|
||||||
|
@ -60,7 +64,11 @@ function createProjectFromTemplate(destPath, newProjectName, templateKey, yarnVe
|
||||||
newProjectName
|
newProjectName
|
||||||
);
|
);
|
||||||
|
|
||||||
if (templateKey !== undefined) {
|
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
|
// Keep the files from the 'HelloWorld' template, and overwrite some of them
|
||||||
// with the specified project template.
|
// with the specified project template.
|
||||||
// The 'HelloWorld' template contains the native files (these are used by
|
// The 'HelloWorld' template contains the native files (these are used by
|
||||||
|
@ -69,29 +77,113 @@ function createProjectFromTemplate(destPath, newProjectName, templateKey, yarnVe
|
||||||
// This way we don't have to duplicate the native files in every template.
|
// 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
|
// 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.
|
// forget to maintain all the copies so they would go out of sync.
|
||||||
const templateName = availableTemplates[templateKey];
|
const builtInTemplateName = builtInTemplates[template];
|
||||||
if (templateName) {
|
if (builtInTemplateName) {
|
||||||
copyProjectTemplateAndReplace(
|
// template is e.g. 'navigation',
|
||||||
path.resolve(
|
// use the built-in local-cli/templates/HelloNavigation folder
|
||||||
'node_modules', 'react-native', 'local-cli', 'templates', templateName
|
createFromBuiltInTemplate(builtInTemplateName, destPath, newProjectName, yarnVersion);
|
||||||
),
|
|
||||||
destPath,
|
|
||||||
newProjectName
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Uknown template: ' + templateKey);
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add dependencies:
|
// Check if the template exists
|
||||||
|
console.log(`Fetching template ${installPackage}...`);
|
||||||
|
try {
|
||||||
|
if (yarnVersion) {
|
||||||
|
execSync(`yarn add ${installPackage} --ignore-scripts`, {stdio: 'inherit'});
|
||||||
|
} else {
|
||||||
|
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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installTemplateDependencies(templatePath, yarnVersion) {
|
||||||
// dependencies.json is a special file that lists additional dependencies
|
// dependencies.json is a special file that lists additional dependencies
|
||||||
// that are required by this template
|
// that are required by this template
|
||||||
const dependenciesJsonPath = path.resolve(
|
const dependenciesJsonPath = path.resolve(
|
||||||
'node_modules', 'react-native', 'local-cli', 'templates', templateName, 'dependencies.json'
|
templatePath, 'dependencies.json'
|
||||||
);
|
);
|
||||||
if (fs.existsSync(dependenciesJsonPath)) {
|
|
||||||
console.log('Adding dependencies for the project...');
|
console.log('Adding dependencies for the project...');
|
||||||
const dependencies = JSON.parse(fs.readFileSync(dependenciesJsonPath));
|
if (!fs.existsSync(dependenciesJsonPath)) {
|
||||||
|
console.log('No additional dependencies.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dependencies;
|
||||||
|
try {
|
||||||
|
dependencies = JSON.parse(fs.readFileSync(dependenciesJsonPath));
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
'Could not parse the template\'s dependencies.json: ' + err.message
|
||||||
|
);
|
||||||
|
}
|
||||||
for (let depName in dependencies) {
|
for (let depName in dependencies) {
|
||||||
const depVersion = dependencies[depName];
|
const depVersion = dependencies[depName];
|
||||||
const depToInstall = depName + '@' + depVersion;
|
const depToInstall = depName + '@' + depVersion;
|
||||||
|
@ -102,8 +194,8 @@ function createProjectFromTemplate(destPath, newProjectName, templateKey, yarnVe
|
||||||
execSync(`npm install ${depToInstall} --save --save-exact`, {stdio: 'inherit'});
|
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 = {
|
module.exports = {
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
* This source code is licensed under the BSD-style license found in the
|
* 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
|
* 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.
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
|
@ -29,6 +31,8 @@ const pollParams = require('./pollParams');
|
||||||
const commandStub = require('./commandStub');
|
const commandStub = require('./commandStub');
|
||||||
const promisify = require('./promisify');
|
const promisify = require('./promisify');
|
||||||
|
|
||||||
|
import type {ConfigT} from '../core';
|
||||||
|
|
||||||
log.heading = 'rnpm-link';
|
log.heading = 'rnpm-link';
|
||||||
|
|
||||||
const dedupeAssets = (assets) => uniq(assets, asset => path.basename(asset));
|
const dedupeAssets = (assets) => uniq(assets, asset => path.basename(asset));
|
||||||
|
@ -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;
|
var project;
|
||||||
try {
|
try {
|
||||||
project = config.getProjectConfig();
|
project = config.getProjectConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(
|
log.error(
|
||||||
'ERRPACKAGEJSON',
|
'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);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
@ -169,8 +175,8 @@ function link(args, config) {
|
||||||
|
|
||||||
return promiseWaterfall(tasks).catch(err => {
|
return promiseWaterfall(tasks).catch(err => {
|
||||||
log.error(
|
log.error(
|
||||||
`It seems something went wrong while linking. Error: ${err.message} \n`
|
`Something went wrong while linking. Error: ${err.message} \n` +
|
||||||
+ 'Please file an issue here: https://github.com/facebook/react-native/issues'
|
'Please file an issue here: https://github.com/facebook/react-native/issues'
|
||||||
);
|
);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
@ -178,6 +184,6 @@ function link(args, config) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
func: link,
|
func: link,
|
||||||
description: 'links all native dependencies',
|
description: 'links all native dependencies (updates native build files)',
|
||||||
name: 'link [packageName]',
|
name: 'link [packageName]',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue