diff --git a/README.md b/README.md index 36d51e2..4de3e7c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +# Dear friends, + +Last November me ([@Kureev](https://github.com/Kureev)) and Mike ([@grabbou](https://github.com/grabbou)) started RNPM. We aimed to bring you a better developer experience and bridge the tooling gap we had back then. Now, as you may know, RNPM is merged into [React Native core](https://github.com/facebook/react-native). It means that from now on you don't need to install a third-party software to use your favorite linking functionality (just use a react-native cli). We'd like to say a big "Thank you!" to everybody who supported us, filed new issues, composed PRs and helped us to review them. + +Now, when RNPM is a part of React Native, we're going to seal this repository and keep working on React Native tooling inside the core. That said, I kindly ask you to file all new issues / prs in react-native repo and cc us. This repo (and other rnpm plugins) will be a available for a few more months in a read-only mode. + +With love, +Alexey Kureev and MichaƂ Grabowski + +![](http://esq.h-cdn.co/assets/16/17/640x360/gallery-1462115295-obama-mic-drop.gif) + + + ![rnpm logo](http://s18.postimg.org/ex7oladjt/logornpm_final4.png) ![npm version](https://img.shields.io/npm/v/rnpm.svg) ![dependencies](https://img.shields.io/david/rnpm/rnpm.svg) [![Code Climate](https://codeclimate.com/github/rnpm/rnpm/badges/gpa.svg)](https://codeclimate.com/github/rnpm/rnpm) [![Test Coverage](https://codeclimate.com/github/rnpm/rnpm/badges/coverage.svg)](https://codeclimate.com/github/rnpm/rnpm/coverage) [![Circle CI](https://img.shields.io/circleci/project/rnpm/rnpm/master.svg)](https://circleci.com/gh/rnpm/rnpm) @@ -100,7 +113,7 @@ Command exported by installed plugin will be available straight away. First of all, every plugin is [just a function](https://github.com/rnpm/rnpm-plugin-link/blob/master/src/link.js#L81) which accepts `config` and `args` parameters. Every plugin consists of [public interface](https://github.com/rnpm/rnpm-plugin-link/blob/master/index.js) for CLI and [implementation intself](https://github.com/rnpm/rnpm-plugin-link/blob/master/src/link.js). We use **public interface** to make your plugins auto-pluggable and easy to use for end-users. Every public interface consists of `name`, `func` & `description` fields: -- `name` - Name of the plugin. After plugin installation it'll be used as a command name. For instance plugin with following interface: +- `name` - Name of the plugin. After plugin installation it'll be used as a command name. For instance a plugin with the following interface: ```javascript module.exports = { func: require('./src/link'), @@ -115,6 +128,23 @@ We use **public interface** to make your plugins auto-pluggable and easy to use - `func` - Plugin itself. This function will be used when you run a command above - `description` - Command description. If user runs `$ rnpm --help`, this field will be displayed as a command description. +- `options` - An array of flags user can specify for your plugin to run. When defined, your exported `func` will receive an object of options as a 3rd argument. For instance a plugin with the following: + + ```js + options: [{ + flags: '-L, --list [path]', + description: 'List flag', + parse: (val) => val.split(',').map(Number), + default: [1,2,3], + }], + ``` + will receive the following object: + ``` + { list: [1,2,3] } + ``` + by default. + + **Note**: `parse` and `default` are optional. You can check [commander.js](https://github.com/tj/commander.js#option-parsing) docs for more information on how to define `flags` value. Also, in the case you want to expose multiple commands from the one plugin, you may use an array syntax: ```javascript @@ -153,19 +183,35 @@ In this scenario we're using custom `prelink` and `postlink` hooks for `rnpm-plu While making your own plugins for `rnpm` you may use any names for the commands, but we strongly recommend you to use a convention we suggest to avoid collisions: `when` + `plugin name`: `prelink` = `pre` + `link`. -### Developers +### Params +On Android - you can specify a custom `packageInstance` to be used when linking your project. The reason for that is often that your package constructor simply requires extra user provided config (e.g. API token). `rnpm` allows you to define an array of additional arguments to get from user during linking process that you can then, reference in your `packageInstance`. -The documentation is still in progress, but if you are interested in the concept and good practices, see sample implementation [here](https://github.com/rnpm/rnpm-plugin-link/blob/master/index.js) +Simply include the following in your package.json: +```json +"rnpm": { + "params": [{ + "type": "input", + "name": "gaToken", + "message": "What's your GA token" + }] +} +``` +and update your `packageInstance` with the new variable: +```json +"rnpm": { + "android": { + "packageInstance": "new SomeLibName(this, ${gaToken})" + } +} +``` -## Roadmap +Starting from now on - users will be presented an interactive form powered by `inquirer` each time they run `rnpm link`. -First priority: **core elements** -- [ ] Test coverage -- [X] Plugins support +**Note**: We pass `params` array directly to inquirer which means you can also let users choose an answer from a list as well as provide a default value! See [API docs](https://github.com/SBoudrias/Inquirer.js/#question) for more details. -Second priority: **new plugins** -- [ ] rnpm ship -- [ ] rnpm build +### Developers + +The documentation is still in progress, but if you are interested in the concept and good practices, see sample implementation [here](https://github.com/rnpm/rnpm-plugin-link/blob/master/index.js) We're open for community ideas! If you know how to improve `rnpm` - please, [let us know](https://github.com/rnpm/rnpm/issues/new)! @@ -216,7 +262,7 @@ Special thanks to [Sonny Lazuardi](https://github.com/sonnylazuardi) for the awe This tool development and maintenance is sponsored by below companies: - + ## License diff --git a/bin/cli b/bin/cli index 774129a..06c8edb 100755 --- a/bin/cli +++ b/bin/cli @@ -12,15 +12,25 @@ updateNotifier({ pkg }).notify(); cli.version(pkg.version); -const addCommand = (command) => - cli +const defaultOptParser = (val) => val; + +const addCommand = (command) => { + const cmd = cli .command(command.name) .usage(command.usage) .description(command.description) .action(function runAction() { - command.func(config, arguments); + command.func(config, arguments, this.opts()); }); + (command.options || []) + .forEach(opt => cmd.option( + opt.flags, + opt.description, + opt.parse || defaultOptParser, + opt.default + )); +}; flatten(commands).forEach(addCommand); diff --git a/package.json b/package.json index c23d1c4..da31a81 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "rnpm", - "version": "1.5.2", + "version": "1.9.0", "description": "React Native Package Manager", - "main": "./src/getActions.js", + "main": "./src/getCommands.js", "scripts": { "test": "jest" }, @@ -39,17 +39,17 @@ "homepage": "https://github.com/rnpm/rnpm#readme", "dependencies": { "commander": "^2.9.0", - "glob": "^6.0.1", + "glob": "^7.0.1", "lodash": "^3.10.1", - "rnpm-plugin-install": "^1.0.0", - "rnpm-plugin-link": "^1.6.0", + "rnpm-plugin-install": "^1.1.0", + "rnpm-plugin-link": "^1.8.0", "update-notifier": "^0.6.0", "xmldoc": "^0.4.0" }, "devDependencies": { "babel-eslint": "^4.1.5", "eslint": "^1.9.0", - "mock-fs": "^3.5.0", + "mock-fs": "^3.9.0", "mock-require": "^1.2.1", "rewire": "^2.5.1", "jest-cli": "^0.9.0-fb2" diff --git a/src/config/android/index.js b/src/config/android/index.js index 7038524..f827730 100644 --- a/src/config/android/index.js +++ b/src/config/android/index.js @@ -38,6 +38,11 @@ exports.projectConfig = function projectConfigAndroid(folder, userConfig) { userConfig.mainActivityPath || `src/main/java/${packageFolder}/MainActivity.java` ); + const stringsPath = path.join( + sourceDir, + userConfig.stringsPath || 'src/main/res/values/strings.xml' + ); + const settingsGradlePath = path.join( folder, 'android', @@ -58,6 +63,7 @@ exports.projectConfig = function projectConfigAndroid(folder, userConfig) { sourceDir, isFlat, folder, + stringsPath, manifestPath, buildGradlePath, settingsGradlePath, diff --git a/src/config/index.js b/src/config/index.js index 7e64d41..318b0be 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -30,12 +30,20 @@ exports.getProjectConfig = function getProjectConfig() { */ exports.getDependencyConfig = function getDependencyConfig(packageName) { const folder = path.join(process.cwd(), 'node_modules', packageName); - const rnpm = getRNPMConfig(folder); + var rnpm; + + // Will throw when package.json is not present in a nested folder + try { + rnpm = getRNPMConfig(folder); + } catch (err) { + rnpm = {}; + } return Object.assign({}, rnpm, { ios: ios.dependencyConfig(folder, rnpm.ios || {}), android: android.dependencyConfig(folder, rnpm.android || {}), assets: findAssets(folder, rnpm.assets), commands: wrapCommands(rnpm.commands), + params: rnpm.params || [], }); }; diff --git a/src/config/ios/findProject.js b/src/config/ios/findProject.js index 0923f0d..17c99cf 100644 --- a/src/config/ios/findProject.js +++ b/src/config/ios/findProject.js @@ -9,7 +9,7 @@ const GLOB_PATTERN = '**/*.xcodeproj'; /** * Regexp matching all test projects */ -const TEST_PROJECTS = /(test|example)/; +const TEST_PROJECTS = /test|example|sample/i; /** * Base iOS folder @@ -19,7 +19,7 @@ const IOS_BASE = 'ios'; /** * These folders will be excluded from search to speed it up */ -const GLOB_EXCLUDE_PATTERN = ['@(Pods|node_modules)/**']; +const GLOB_EXCLUDE_PATTERN = ['**/@(Pods|node_modules)/**']; /** * Finds iOS project by looking for all .xcodeproj files @@ -36,8 +36,7 @@ module.exports = function findProject(folder) { ignore: GLOB_EXCLUDE_PATTERN, }) .filter(project => { - const xcPath = project.toLowerCase(); - return path.dirname(xcPath) === IOS_BASE || !xcPath.match(TEST_PROJECTS); + return path.dirname(project) === IOS_BASE || !TEST_PROJECTS.test(project); }); if (projects.length === 0) { diff --git a/src/config/ios/index.js b/src/config/ios/index.js index 3171504..5e1f2c0 100644 --- a/src/config/ios/index.js +++ b/src/config/ios/index.js @@ -1,6 +1,19 @@ const path = require('path'); const findProject = require('./findProject'); +/** + * For libraries specified without an extension, add '.tbd' for those that + * start with 'lib' and '.framework' to the rest. + */ +const mapSharedLibaries = (libraries) => { + return libraries.map(name => { + if (path.extname(name)) { + return name; + } + return name + (name.indexOf('lib') === 0 ? '.tbd' : '.framework'); + }); +}; + /** * Returns project config by analyzing given folder and applying some user defaults * when constructing final object @@ -24,6 +37,8 @@ exports.projectConfig = function projectConfigIOS(folder, userConfig) { projectPath: projectPath, projectName: path.basename(projectPath), libraryFolder: userConfig.libraryFolder || 'Libraries', + sharedLibraries: mapSharedLibaries(userConfig.sharedLibraries || []), + plist: userConfig.plist || [], }; }; diff --git a/src/findPlugins.js b/src/findPlugins.js index 43e626d..8cb8818 100644 --- a/src/findPlugins.js +++ b/src/findPlugins.js @@ -9,12 +9,29 @@ const flatten = require('lodash').flatten; * @param {String} dependency Name of the dependency * @return {Boolean} If dependency is a rnpm plugin */ -const isPlugin = (dependency) => !!~dependency.indexOf('rnpm-plugin-'); +const isRNPMPlugin = (dependency) => !!~dependency.indexOf('rnpm-plugin-'); +const isReactNativePlugin = (dependency) => !!~dependency.indexOf('react-native-'); -const findPluginInFolder = (folder) => { +const readPackage = (folder) => { try { - const pjson = require(path.join(folder, 'package.json')); + return require(path.join(folder, 'package.json')); } catch (e) { + return null; + } +}; + +const findPluginsInReactNativePackage = (pjson) => { + if (!pjson.rnpm || !pjson.rnpm.plugin) { + return []; + } + + return path.join(pjson.name, pjson.rnpm.plugin); +}; + +const findPluginInFolder = (folder) => { + const pjson = readPackage(folder); + + if (!pjson) { return []; } @@ -23,7 +40,22 @@ const findPluginInFolder = (folder) => { Object.keys(pjson.devDependencies || {}) ); - return deps.filter(isPlugin); + return deps.reduce( + (acc, pkg) => { + if (isRNPMPlugin(pkg)) { + return acc.concat(pkg); + } + if (isReactNativePlugin(pkg)) { + const pjson = readPackage(path.join(folder, 'node_modules', pkg)); + if (!pjson) { + return acc; + } + return acc.concat(findPluginsInReactNativePackage(pjson)); + } + return acc; + }, + [] + ); }; /** diff --git a/test/fixtures/ios.js b/test/fixtures/ios.js index f3382db..5094355 100644 --- a/test/fixtures/ios.js +++ b/test/fixtures/ios.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); exports.valid = { - 'sampleProject.xcodeproj': { + 'demoProject.xcodeproj': { 'project.pbxproj': fs.readFileSync(path.join(__dirname, './files/project.pbxproj')), }, }; diff --git a/test/ios/findProject.spec.js b/test/ios/findProject.spec.js index f7a3593..6626921 100644 --- a/test/ios/findProject.spec.js +++ b/test/ios/findProject.spec.js @@ -32,6 +32,16 @@ describe('ios::findProject', () => { expect(findProject('')).toBe(null); }); + it('should ignore Pods inside `ios` folder', () => { + mockFs({ + ios: { + Pods: projects.flat, + DemoApp: projects.flat.ios, + }, + }); + expect(findProject('')).toBe('ios/DemoApp/demoProject.xcodeproj'); + }); + it('should ignore xcodeproj from example folders', () => { mockFs({ examples: projects.flat, @@ -44,6 +54,18 @@ describe('ios::findProject', () => { expect(findProject('').toLowerCase()).not.toContain('example'); }); + it('should ignore xcodeproj from sample folders', () => { + mockFs({ + samples: projects.flat, + Samples: projects.flat, + sample: projects.flat, + KeychainSample: projects.flat, + Zpp: projects.flat, + }); + + expect(findProject('').toLowerCase()).not.toContain('sample'); + }); + it('should ignore xcodeproj from test folders at any level', () => { mockFs({ test: projects.flat, diff --git a/test/ios/getProjectConfig.spec.js b/test/ios/getProjectConfig.spec.js index bba3bed..82a2245 100644 --- a/test/ios/getProjectConfig.spec.js +++ b/test/ios/getProjectConfig.spec.js @@ -22,5 +22,15 @@ describe('ios::getProjectConfig', () => { expect(getProjectConfig(folder, userConfig)).toBe(null); }); + it('should return normalized shared library names', () => { + const projectConfig = getProjectConfig('testDir/nested', { + sharedLibraries: ['libc++', 'libz.tbd', 'HealthKit', 'HomeKit.framework'], + }); + + expect(projectConfig.sharedLibraries).toEqual( + ['libc++.tbd', 'libz.tbd', 'HealthKit.framework', 'HomeKit.framework'] + ); + }); + afterEach(mockFs.restore); });