diff --git a/local-cli/generator/copyProjectTemplateAndReplace.js b/local-cli/generator/copyProjectTemplateAndReplace.js index c24a3d3b0..4d2725882 100644 --- a/local-cli/generator/copyProjectTemplateAndReplace.js +++ b/local-cli/generator/copyProjectTemplateAndReplace.js @@ -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'); } @@ -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) => { diff --git a/local-cli/generator/templates.js b/local-cli/generator/templates.js index dfbf598e2..cc7145c23 100644 --- a/local-cli/generator/templates.js +++ b/local-cli/generator/templates.js @@ -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', }; @@ -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 ' + @@ -47,12 +50,13 @@ 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'), @@ -60,52 +64,140 @@ function createProjectFromTemplate(destPath, newProjectName, templateKey, yarnVe 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'}); } - - // 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' + const templatePath = path.resolve( + 'node_modules', templateName ); - 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'}); - } + 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 + // 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; + } + + 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) { + 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 = { listTemplatesAndExit, createProjectFromTemplate, diff --git a/local-cli/link/link.js b/local-cli/link/link.js index 8b8aa616e..f597c9079 100644 --- a/local-cli/link/link.js +++ b/local-cli/link/link.js @@ -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'); @@ -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)); @@ -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, 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); } @@ -169,8 +175,8 @@ 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; }); @@ -178,6 +184,6 @@ function link(args, config) { module.exports = { func: link, - description: 'links all native dependencies', + description: 'links all native dependencies (updates native build files)', name: 'link [packageName]', };