diff --git a/.circleci/config.yml b/.circleci/config.yml index 53e390b7e..ec7d9ef57 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,11 +14,11 @@ jobs: at: ~/react-native-cli - restore_cache: keys: - - dependencies-{{ checksum "package.json" }} - - dependencies- + - v2-dependencies-{{ checksum "package.json" }} + - v2-dependencies- - run: yarn install - save_cache: - key: dependencies-{{ checksum "package.json" }} + key: v2-dependencies-{{ checksum "package.json" }} paths: - node_modules - persist_to_workspace: diff --git a/.flowconfig b/.flowconfig index be12bc31a..87fb9288f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -43,6 +43,7 @@ module.ignore_non_literal_requires=true all=warn unnecessary-optional-chain=off dynamic-export=off +unsafe-getters-setters=off # There is an ESLint rule for this unclear-type=off diff --git a/e2e/__tests__/uninstall.test.js b/e2e/__tests__/uninstall.test.js index 1c9f6df05..1b9053e49 100644 --- a/e2e/__tests__/uninstall.test.js +++ b/e2e/__tests__/uninstall.test.js @@ -38,7 +38,7 @@ test('uninstall fails when package is not installed', () => { }); const {stderr, code} = run(DIR, ['uninstall', pkg]); - expect(stderr).toContain(`Project "${pkg}" is not a react-native library`); + expect(stderr).toContain(`Failed to unlink "${pkg}".`); expect(code).toBe(1); }); diff --git a/packages/cli/src/cliEntry.js b/packages/cli/src/cliEntry.js index 0b44c5fe4..fded0f87f 100644 --- a/packages/cli/src/cliEntry.js +++ b/packages/cli/src/cliEntry.js @@ -18,7 +18,6 @@ import {getCommands} from './commands'; import init from './commands/init/initCompat'; import assertRequiredOptions from './tools/assertRequiredOptions'; import logger from './tools/logger'; -import findPlugins from './tools/findPlugins'; import {setProjectDir} from './tools/PackageManager'; import pkgJson from '../package.json'; import loadConfig from './tools/config'; @@ -38,15 +37,17 @@ const handleError = err => { if (commander.verbose) { logger.error(err.message); } else { + // Some error messages (esp. custom ones) might have `.` at the end already. + const message = err.message.replace(/\.$/, ''); logger.error( - `${err.message}. ${chalk.dim( + `${message}. ${chalk.dim( `Run CLI with ${chalk.reset('--verbose')} ${chalk.dim( 'flag for more details.', )}`, )}`, ); } - logger.debug(err.stack); + logger.debug(chalk.dim(err.stack)); process.exit(1); }; @@ -184,7 +185,7 @@ async function setupAndRun() { export default { run, init, - findPlugins, + loadConfig, }; -export {run, init, findPlugins}; +export {run, init, loadConfig}; diff --git a/packages/cli/src/commands/info/__tests__/info.test.js b/packages/cli/src/commands/info/__tests__/info.test.js index 12dc9383f..cbda1f26f 100644 --- a/packages/cli/src/commands/info/__tests__/info.test.js +++ b/packages/cli/src/commands/info/__tests__/info.test.js @@ -1,6 +1,7 @@ // @flow import info from '../info'; import logger from '../../../tools/logger'; +import loadConfig from '../../../tools/config'; jest.mock('../../../tools/logger', () => ({ info: jest.fn(), @@ -8,24 +9,16 @@ jest.mock('../../../tools/logger', () => ({ log: jest.fn(), })); -const ctx = { - root: '', - reactNativePath: '', - dependencies: {}, - platforms: {}, - commands: [], - haste: { - platforms: [], - providesModuleNodeModules: [], - }, -}; +jest.mock('../../../tools/config'); beforeEach(() => { jest.resetAllMocks(); }); +const config = loadConfig(); + test('prints output without arguments', async () => { - await info.func([], ctx, {}); + await info.func([], config, {}); expect(logger.info).toHaveBeenCalledWith( 'Fetching system and libraries information...', ); diff --git a/packages/cli/src/commands/init/init.js b/packages/cli/src/commands/init/init.js index 7b8486723..24c8b965a 100644 --- a/packages/cli/src/commands/init/init.js +++ b/packages/cli/src/commands/init/init.js @@ -100,9 +100,9 @@ function createProject(projectName: string, options: Options, version: string) { return createFromReactNativeTemplate(projectName, version, options.npm); } -export default async function initialize( +export default (async function initialize( [projectName]: Array, - context: ContextT, + _context: ContextT, options: Options, ) { validateProjectName(projectName); @@ -125,4 +125,4 @@ export default async function initialize( logger.error(e.message); fs.removeSync(projectName); } -} +}); diff --git a/packages/cli/src/commands/install/install.js b/packages/cli/src/commands/install/install.js index bd9097a0c..ffcb7f0df 100644 --- a/packages/cli/src/commands/install/install.js +++ b/packages/cli/src/commands/install/install.js @@ -11,6 +11,7 @@ import type {ContextT} from '../../tools/types.flow'; import logger from '../../tools/logger'; import * as PackageManager from '../../tools/PackageManager'; import link from '../link/link'; +import loadConfig from '../../tools/config'; async function install(args: Array, ctx: ContextT) { const name = args[0]; @@ -18,8 +19,11 @@ async function install(args: Array, ctx: ContextT) { logger.info(`Installing "${name}"...`); PackageManager.install([name]); + // Reload configuration to see newly installed dependency + const newConfig = loadConfig(); + logger.info(`Linking "${name}"...`); - await link.func([name], ctx, {platforms: undefined}); + await link.func([name], newConfig, {platforms: undefined}); logger.success(`Successfully installed and linked "${name}"`); } diff --git a/packages/cli/src/commands/link/__tests__/__snapshots__/getDependencyConfig-test.js.snap b/packages/cli/src/commands/link/__tests__/__snapshots__/getDependencyConfig-test.js.snap deleted file mode 100644 index 9fc3eecc9..000000000 --- a/packages/cli/src/commands/link/__tests__/__snapshots__/getDependencyConfig-test.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`getDependencyConfig should return an array of dependencies' config 1`] = ` -Object { - "assets": Array [], - "commands": Object {}, - "config": Object { - "android": Object { - "sampleAndroidKey": "", - }, - "ios": Object { - "sampleiOSKey": "", - }, - }, - "name": "react-native-windows", - "params": Array [], - "path": "/root/node_modules/react-native-windows", -} -`; diff --git a/packages/cli/src/commands/link/__tests__/getDependencyConfig-test.js b/packages/cli/src/commands/link/__tests__/getDependencyConfig-test.js deleted file mode 100644 index 73cd1f7a2..000000000 --- a/packages/cli/src/commands/link/__tests__/getDependencyConfig-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @emails oncall+javascript_foundation - */ - -const platforms = { - ios: { - dependencyConfig: () => ({sampleiOSKey: ''}), - }, - android: { - dependencyConfig: () => ({sampleAndroidKey: ''}), - }, -}; - -jest.setMock('../../../tools/getPackageConfiguration', folder => { - if (folder === '/root/node_modules/abcd') { - throw new Error('Cannot require'); - } - return {}; -}); - -const getDependencyConfig = require('../getDependencyConfig').default; - -describe('getDependencyConfig', () => { - it("should return an array of dependencies' config", () => { - const dependencies = getDependencyConfig( - {root: '/root'}, - platforms, - 'react-native-windows', - ); - - expect(dependencies).toMatchSnapshot(); - }); - - it('should throw on invalid react-native dependency', () => { - expect(() => - getDependencyConfig({root: '/root'}, platforms, 'abcd'), - ).toThrowError(); - }); -}); diff --git a/packages/cli/src/commands/link/__tests__/getProjectDependencies-test.js b/packages/cli/src/commands/link/__tests__/getProjectDependencies-test.js deleted file mode 100644 index 5eac43b0b..000000000 --- a/packages/cli/src/commands/link/__tests__/getProjectDependencies-test.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @emails oncall+javascript_foundation - * @format - */ - -import getProjectDependencies from '../getProjectDependencies'; - -const path = require('path'); - -const CWD = path.resolve(__dirname, '../../'); - -describe('getProjectDependencies', () => { - beforeEach(() => { - jest.resetModules(); - }); - it('should return an array of project dependencies', () => { - jest.doMock( - path.join(CWD, './package.json'), - () => ({ - dependencies: { - lodash: '^6.0.0', - 'react-native': '^16.0.0', - '@react-native-community/cli': '*', - }, - }), - {virtual: true}, - ); - - expect(getProjectDependencies(CWD)).toEqual(['lodash']); - }); - - it('should return an empty array when no dependencies set', () => { - jest.doMock(path.join(CWD, './package.json'), () => ({}), { - virtual: true, - }); - expect(getProjectDependencies(CWD)).toEqual([]); - }); -}); diff --git a/packages/cli/src/commands/link/__tests__/link-test.js b/packages/cli/src/commands/link/__tests__/link-test.js index 59e905d9f..c409283f3 100644 --- a/packages/cli/src/commands/link/__tests__/link-test.js +++ b/packages/cli/src/commands/link/__tests__/link-test.js @@ -1,18 +1,18 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @emails oncall+javascript_foundation - */ +import {func as link} from '../link'; +import loadConfig from '../../../tools/config'; jest.mock('chalk', () => ({grey: str => str})); +jest.mock('../../../tools/config'); jest.mock('../../../tools/logger'); -const context = { - root: process.cwd(), +const baseConfig = loadConfig(); + +const baseDependencyConfig = { + name: 'react-native-gradient', + assets: [], + hooks: {}, + params: [], + platforms: {ios: {}, android: {}}, }; describe('link', () => { @@ -20,89 +20,74 @@ describe('link', () => { jest.resetModules(); }); - it('should reject when run in a folder without package.json', done => { - const link = require('../link').func; - link([], {root: '/'}, {}).catch(() => done()); - }); - - it('should accept a name of a dependency to link', done => { - const getDependencyConfig = jest.fn(() => ({ - config: { - ios: null, - android: null, + it('should accept a name of a dependency to link', async () => { + const config = { + ...baseConfig, + dependencies: { + get ['react-native-gradient']() { + return baseDependencyConfig; + }, }, - assets: [], - commands: {}, - })); + }; - jest.doMock('../getDependencyConfig', () => getDependencyConfig); + const spy = jest.spyOn(config.dependencies, 'react-native-gradient', 'get'); - const link = require('../link').func; - link(['react-native-gradient'], context, {}).then(() => { - expect(getDependencyConfig.mock.calls[0][2]).toEqual( - 'react-native-gradient', - ); - done(); - }); + await link(['react-native-gradient'], config, {}); + + expect(spy).toHaveBeenCalled(); }); it('should accept the name of a dependency with a scope / tag', async () => { - const getDependencyConfig = jest.fn(() => ({ - config: { - ios: null, - android: null, + const config = { + ...baseConfig, + dependencies: { + get ['@scope/something']() { + return baseDependencyConfig; + }, }, - assets: [], - commands: {}, - })); + }; - jest.doMock('../getDependencyConfig', () => getDependencyConfig); + const spy = jest.spyOn(config.dependencies, '@scope/something', 'get'); - const link = require('../link').func; - await link(['@scope/something@latest'], context, {}); + await link(['@scope/something@latest'], config, {}); - expect(getDependencyConfig.mock.calls[0][2]).toEqual('@scope/something'); + expect(spy).toHaveBeenCalled(); }); it('should register native module when android/ios projects are present', done => { const prelink = jest.fn().mockImplementation(cb => cb()); const postlink = jest.fn().mockImplementation(cb => cb()); + const registerNativeModule = jest.fn(); - jest.doMock('../getProjectConfig', () => () => ({ - ios: {}, - android: {}, - })); - - const getDependencyConfig = jest.fn(() => ({ - config: { + const config = { + ...baseConfig, + project: { ios: {}, android: {}, }, - assets: [], - commands: {prelink, postlink}, - })); - - jest.doMock('../getDependencyConfig', () => getDependencyConfig); - - const registerNativeModule = jest.fn(); - - jest.doMock('../android/isInstalled.js', () => - jest.fn().mockReturnValue(false), - ); - jest.doMock( - '../android/registerNativeModule.js', - () => registerNativeModule, - ); - - jest.doMock('../ios/isInstalled.js', () => - jest.fn().mockReturnValue(false), - ); - jest.doMock('../ios/registerNativeModule.js', () => registerNativeModule); - - const link = require('../link').func; - registerNativeModule.mockClear(); + platforms: { + ios: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(false), + }), + }, + android: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(false), + }), + }, + }, + dependencies: { + 'react-native-blur': { + ...baseDependencyConfig, + hooks: {prelink, postlink}, + }, + }, + }; - link(['react-native-blur'], context, {}).then(() => { + link(['react-native-blur'], config, {}).then(() => { expect(registerNativeModule.mock.calls).toHaveLength(2); expect(prelink.mock.invocationCallOrder[0]).toBeLessThan( @@ -121,199 +106,158 @@ describe('link', () => { const dependencyAssets = ['Fonts/Font.ttf']; const projectAssets = ['Fonts/FontC.ttf']; - jest.doMock('../getProjectConfig', () => () => ({ - ios: {}, - android: {}, - })); + const copyAssets = jest.fn(); + const dependency = { + ...baseDependencyConfig, + assets: dependencyAssets, + }; - jest.doMock('../getDependencyConfig', () => () => ({ - config: { + const config = { + ...baseConfig, + project: { ios: {}, android: {}, }, - assets: dependencyAssets, - commands: {}, - })); - - jest.doMock('../android/isInstalled.js', () => - jest.fn().mockReturnValue(false), - ); - jest.doMock('../android/registerNativeModule.js', () => jest.fn()); - - jest.doMock('../ios/isInstalled.js', () => - jest.fn().mockReturnValue(false), - ); - jest.doMock('../ios/registerNativeModule.js', () => jest.fn()); - - jest.doMock('../../../tools/getAssets', () => projectAssets); - - const copyAssets = jest.fn(); - - jest.doMock('../ios/copyAssets.js', () => copyAssets); - jest.doMock('../android/copyAssets.js', () => copyAssets); - - const link = require('../link').func; + platforms: { + ios: { + linkConfig: () => ({ + register: jest.fn(), + copyAssets, + isInstalled: jest.fn().mockReturnValue(false), + }), + }, + android: { + linkConfig: () => ({ + register: jest.fn(), + copyAssets, + isInstalled: jest.fn().mockReturnValue(false), + }), + }, + }, + dependencies: { + 'react-native-blur': dependency, + }, + assets: projectAssets, + }; - link(['react-native-blur'], context, {}).then(() => { + link(['react-native-blur'], config, {}).then(() => { expect(copyAssets.mock.calls).toHaveLength(2); expect(copyAssets.mock.calls[0][0]).toEqual(dependencyAssets); - jest.unmock('../../../tools/getAssets'); done(); }); }); it('should not register modules when they are already installed', done => { - jest.doMock('../getProjectConfig', () => () => ({ - ios: {}, - android: {}, - })); + const registerNativeModule = jest.fn(); - const getDependencyConfig = jest.fn(() => ({ - config: { + const config = { + ...baseConfig, + project: { ios: {}, android: {}, }, - assets: [], - commands: {}, - })); - - jest.doMock('../getDependencyConfig', () => getDependencyConfig); - - const registerNativeModule = jest.fn(); - - jest.doMock('../android/isInstalled.js', () => - jest.fn().mockReturnValue(true), - ); - jest.doMock( - '../android/registerNativeModule.js', - () => registerNativeModule, - ); - - jest.doMock('../ios/isInstalled.js', () => jest.fn().mockReturnValue(true)); - jest.doMock('../ios/registerNativeModule.js', () => registerNativeModule); - - const link = require('../link').func; + platforms: { + ios: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(true), + }), + }, + android: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(true), + }), + }, + }, + dependencies: { + 'react-native-blur': baseDependencyConfig, + }, + }; - link(['react-native-blur', {}], context, {}).then(() => { + link(['react-native-blur'], config, {}).then(() => { expect(registerNativeModule.mock.calls).toHaveLength(0); done(); }); }); it('should register native modules for additional platforms', done => { - jest.doMock('../getProjectConfig', () => () => ({ - ios: {}, - android: {}, - windows: {}, - })); - const registerNativeModule = jest.fn(); - const genericLinkConfig = () => ({ - isInstalled: () => false, - register: registerNativeModule, - }); - - const getDependencyConfig = jest.fn(() => ({ - config: { + const config = { + ...baseConfig, + project: { ios: {}, android: {}, windows: {}, }, - assets: [], - commands: {}, - })); - - jest.doMock('../../../tools/getPlatforms', () => { - const fn = () => ({ - ios: {linkConfig: require('../ios').default}, - android: {linkConfig: require('../android').default}, - windows: {linkConfig: genericLinkConfig}, - }); - fn.getPlatformName = jest.fn(); - return fn; - }); - - jest.doMock('../getDependencyConfig', () => getDependencyConfig); - - jest.doMock('../android/isInstalled.js', () => - jest.fn().mockReturnValue(true), - ); - jest.doMock( - '../android/registerNativeModule.js', - () => registerNativeModule, - ); - - jest.doMock('../ios/isInstalled.js', () => jest.fn().mockReturnValue(true)); - jest.doMock('../ios/registerNativeModule.js', () => registerNativeModule); + platforms: { + ios: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(true), + }), + }, + android: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(true), + }), + }, + windows: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(false), + }), + }, + }, + dependencies: { + 'react-native-blur': { + ...baseDependencyConfig, + platforms: { + ...baseDependencyConfig.platforms, + windows: {}, + }, + }, + }, + }; - const link = require('../link').func; - link(['react-native-blur'], context, {}).then(() => { + link(['react-native-blur'], config, {}).then(() => { expect(registerNativeModule.mock.calls).toHaveLength(1); done(); }); }); it('should link only for specific platforms if --platforms is used', async () => { - jest.doMock('../getProjectDependencies', () => () => ['react-native-maps']); - jest.doMock('../../../tools/getPackageConfiguration', () => () => ({ - assets: [], - })); - - const registerAndroidNativeModule = jest.fn(); - const registerIOSNativeModule = jest.fn(); - - const genericAndroidLinkConfig = () => ({ - isInstalled: () => false, - register: registerAndroidNativeModule, - }); - - const genericIOSLinkConfig = () => ({ - isInstalled: () => false, - register: registerIOSNativeModule, - }); - - jest.doMock('../../../tools/getPlatforms', () => { - const fn = () => ({ - android: {linkConfig: genericAndroidLinkConfig}, - ios: {linkConfig: genericIOSLinkConfig}, - }); - fn.getPlatformName = jest.fn(); - return fn; - }); + const registerNativeModule = jest.fn(); - jest.doMock( - '../android/registerNativeModule.js', - () => registerAndroidNativeModule, - ); - jest.doMock( - '../ios/registerNativeModule.js', - () => registerIOSNativeModule, - ); - - const link = require('../link').func; - const assertPlaftormsCalledTimes = (android, ios) => { - expect(registerAndroidNativeModule).toHaveBeenCalledTimes(android); - expect(registerIOSNativeModule).toHaveBeenCalledTimes(ios); - registerAndroidNativeModule.mockClear(); - registerIOSNativeModule.mockClear(); + const config = { + ...baseConfig, + project: { + ios: {}, + android: {}, + }, + platforms: { + ios: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(false), + }), + }, + android: { + linkConfig: () => ({ + register: registerNativeModule, + isInstalled: jest.fn().mockReturnValue(false), + }), + }, + }, + dependencies: { + 'react-native-blur': baseDependencyConfig, + }, }; - await link( - ['react-native-gradient'], - {root: '/'}, - {platforms: ['android']}, - ); - assertPlaftormsCalledTimes(1, 0); - - await link(['react-native-gradient'], {root: '/'}, {platforms: ['ios']}); - assertPlaftormsCalledTimes(0, 1); - - await link( - ['react-native-gradient'], - {root: '/'}, - {platforms: ['android', 'ios']}, - ); - assertPlaftormsCalledTimes(1, 1); + await link(['react-native-blur'], config, {platforms: ['android']}); + + expect(registerNativeModule.mock.calls).toHaveLength(1); }); }); diff --git a/packages/cli/src/commands/link/getDependencyConfig.js b/packages/cli/src/commands/link/getDependencyConfig.js deleted file mode 100644 index 7983691c2..000000000 --- a/packages/cli/src/commands/link/getDependencyConfig.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @flow - */ - -import path from 'path'; -import type { - PlatformsT, - ContextT, - DependenciesConfig, -} from '../../tools/types.flow'; - -import getPackageConfiguration from '../../tools/getPackageConfiguration'; -import getParams from '../../tools/getParams'; -import getHooks from '../../tools/getHooks'; -import getAssets from '../../tools/getAssets'; - -export default function getDependencyConfig( - ctx: ContextT, - availablePlatforms: PlatformsT, - dependency: string, -): DependenciesConfig { - try { - const folder = path.join(ctx.root, 'node_modules', dependency); - const config = getPackageConfiguration(folder); - - const platformConfigs = {ios: undefined, android: undefined}; - - Object.keys(availablePlatforms).forEach(platform => { - platformConfigs[platform] = availablePlatforms[platform].dependencyConfig( - folder, - // $FlowIssue: Flow can't match platform config with its appropriate config function - config[platform] || {}, - ); - }); - - return { - config: platformConfigs, - name: dependency, - path: folder, - commands: getHooks(folder), - assets: getAssets(folder), - params: getParams(folder), - }; - } catch (e) { - throw new Error('Failed to get dependency config'); - } -} diff --git a/packages/cli/src/commands/link/getPlatformName.js b/packages/cli/src/commands/link/getPlatformName.js new file mode 100644 index 000000000..455d9c06c --- /dev/null +++ b/packages/cli/src/commands/link/getPlatformName.js @@ -0,0 +1,12 @@ +/** + * @flow + */ + +const names = { + ios: 'iOS', + android: 'Android', +}; + +export default function getPlatformName(name: string) { + return names[name] || name; +} diff --git a/packages/cli/src/commands/link/getProjectConfig.js b/packages/cli/src/commands/link/getProjectConfig.js deleted file mode 100644 index d3b40c169..000000000 --- a/packages/cli/src/commands/link/getProjectConfig.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @flow - */ - -import type { - PlatformsT, - ContextT, - ProjectConfigT, -} from '../../tools/types.flow'; - -import getPackageConfiguration from '../../tools/getPackageConfiguration'; -import {getPlatformName} from '../../tools/getPlatforms'; -import logger from '../../tools/logger'; - -export default function getProjectConfig( - ctx: ContextT, - availablePlatforms: PlatformsT, -): ProjectConfigT { - const config = getPackageConfiguration(ctx.root); - - const platformConfigs = {ios: undefined, android: undefined}; - - Object.keys(availablePlatforms).forEach(platform => { - logger.debug(`Getting project config for ${getPlatformName(platform)}...`); - platformConfigs[platform] = availablePlatforms[platform].projectConfig( - ctx.root, - // $FlowIssue: Flow can't match platform config with its appropriate config function - config[platform] || {}, - ); - }); - - return platformConfigs; -} diff --git a/packages/cli/src/commands/link/getProjectDependencies.js b/packages/cli/src/commands/link/getProjectDependencies.js deleted file mode 100644 index 9d6c3d4f3..000000000 --- a/packages/cli/src/commands/link/getProjectDependencies.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import path from 'path'; - -/** - * List of projects that should not be treated as projects to be linked. - * - * That includes `react-native` itself and the CLI project (under its real and staging npm package). - */ -const EXCLUDED_PROJECTS = [ - 'react-native', - '@react-native-community/cli', - 'react-native-local-cli-preview', -]; - -/** - * Returns an array of dependencies that should be linked/checked. - */ -export default function getProjectDependencies(cwd: string) { - const pkgJson = require(path.join(cwd, './package.json')); - return (Object.keys(pkgJson.dependencies || {}).filter( - name => EXCLUDED_PROJECTS.includes(name) === false, - ): Array); -} diff --git a/packages/cli/src/commands/link/ios/common/unregisterNativeModule.js b/packages/cli/src/commands/link/ios/common/unregisterNativeModule.js index 482f3ef67..61752d42c 100644 --- a/packages/cli/src/commands/link/ios/common/unregisterNativeModule.js +++ b/packages/cli/src/commands/link/ios/common/unregisterNativeModule.js @@ -22,7 +22,9 @@ export default function unregisterNativeModule( const isIosInstalled = isInstalledIOS(projectConfig, dependencyConfig); const isPodInstalled = isInstalledPods(projectConfig, dependencyConfig); if (isIosInstalled) { - const iOSDependencies = compact(otherDependencies.map(d => d.config.ios)); + const iOSDependencies = compact( + otherDependencies.map(d => d.platforms.ios), + ); unregisterDependencyIOS(dependencyConfig, projectConfig, iOSDependencies); } else if (isPodInstalled) { unregisterDependencyPods(dependencyConfig, projectConfig); diff --git a/packages/cli/src/commands/link/link.js b/packages/cli/src/commands/link/link.js index c57f950ac..0afddc112 100644 --- a/packages/cli/src/commands/link/link.js +++ b/packages/cli/src/commands/link/link.js @@ -8,18 +8,21 @@ */ import {pick} from 'lodash'; -import type {ContextT} from '../../tools/types.flow'; +import dedent from 'dedent'; + +import {type ContextT} from '../../tools/types.flow'; + +import {CLIError} from '../../tools/errors'; import promiseWaterfall from './promiseWaterfall'; import logger from '../../tools/logger'; -import getDependencyConfig from './getDependencyConfig'; import commandStub from './commandStub'; import promisify from './promisify'; -import getProjectConfig from './getProjectConfig'; +import getPlatformName from './getPlatformName'; + import linkDependency from './linkDependency'; import linkAssets from './linkAssets'; import linkAll from './linkAll'; -import getPlatforms, {getPlatformName} from '../../tools/getPlatforms'; type FlagsType = { platforms?: Array, @@ -32,60 +35,54 @@ type FlagsType = { * only that package is processed. */ function link([rawPackageName]: Array, ctx: ContextT, opts: FlagsType) { - let platforms; - let project; - try { - platforms = getPlatforms(ctx.root); - logger.debug( - 'Available platforms: ' + - `${Object.getOwnPropertyNames(platforms) - .map(platform => getPlatformName(platform)) - .join(', ')}`, - ); - if (opts.platforms) { - platforms = pick(platforms, opts.platforms); - } - logger.debug( - 'Targeted platforms: ' + - `${Object.getOwnPropertyNames(platforms) - .map(platform => getPlatformName(platform)) - .join(', ')}`, - ); - project = getProjectConfig(ctx, platforms); - } catch (err) { - logger.error( - 'No package found. Are you sure this is a React Native project?', - ); - return Promise.reject(err); + let platforms = ctx.platforms; + let project = ctx.project; + + if (opts.platforms) { + platforms = pick(platforms, opts.platforms); + logger.debug('Skipping selected platforms'); } + logger.debug( + 'Available platforms: ' + + `${Object.keys(platforms) + .map(getPlatformName) + .join(', ')}`, + ); + if (rawPackageName === undefined) { logger.debug( 'No package name provided, will attemp to link all possible packages.', ); - return linkAll(ctx, platforms, project); + return linkAll(ctx); } - logger.debug(`Package to link: ${rawPackageName}`); - // Trim the version / tag out of the package name (eg. package@latest) const packageName = rawPackageName.replace(/^(.+?)(@.+?)$/gi, '$1'); - const dependencyConfig = getDependencyConfig(ctx, platforms, packageName); + if (!Object.keys(ctx.dependencies).includes(packageName)) { + throw new CLIError(dedent` + Unknown dependency. Make sure that the package you are trying to link is + already installed in your "node_modules" and present in your "package.json" dependencies. + `); + } + + const {[packageName]: dependency} = ctx.dependencies; + + logger.debug(`Package to link: ${rawPackageName}`); const tasks = [ - () => promisify(dependencyConfig.commands.prelink || commandStub), - () => linkDependency(platforms, project, dependencyConfig), - () => promisify(dependencyConfig.commands.postlink || commandStub), - () => linkAssets(platforms, project, dependencyConfig.assets), + () => promisify(dependency.hooks.prelink || commandStub), + () => linkDependency(platforms, project, dependency), + () => promisify(dependency.hooks.postlink || commandStub), + () => linkAssets(platforms, project, dependency.assets), ]; return promiseWaterfall(tasks).catch(err => { - logger.error( - `Something went wrong while linking. Error: ${err.message} \n` + - 'Please file an issue here: https://github.com/react-native-community/react-native-cli/issues', + throw new CLIError( + `Something went wrong while linking. Reason: ${err.message}`, + err, ); - throw err; }); } @@ -104,5 +101,3 @@ export default { }, ], }; - -// link; diff --git a/packages/cli/src/commands/link/linkAll.js b/packages/cli/src/commands/link/linkAll.js index 9ba413763..bd9f80a80 100644 --- a/packages/cli/src/commands/link/linkAll.js +++ b/packages/cli/src/commands/link/linkAll.js @@ -1,64 +1,48 @@ -// @flow +/** + * @flow + */ -import {uniqBy, flatten} from 'lodash'; +import {uniqBy, flatMap} from 'lodash'; import path from 'path'; -import type { - ContextT, - PlatformsT, - ProjectConfigT, -} from '../../tools/types.flow'; -import logger from '../../tools/logger'; -import getAssets from '../../tools/getAssets'; -import getProjectDependencies from './getProjectDependencies'; -import getDependencyConfig from './getDependencyConfig'; + +import type {ConfigT, PlatformsT} from '../../tools/config/types.flow'; +import {CLIError} from '../../tools/errors'; + import promiseWaterfall from './promiseWaterfall'; import commandStub from './commandStub'; import promisify from './promisify'; + import linkAssets from './linkAssets'; import linkDependency from './linkDependency'; const dedupeAssets = (assets: Array): Array => uniqBy(assets, asset => path.basename(asset)); -function linkAll( - context: ContextT, - platforms: PlatformsT, - project: ProjectConfigT, -) { - logger.warn( - 'Running `react-native link` without package name is deprecated and will be removed ' + - 'in next release. If you use this command to link your project assets, ' + - 'please let us know about your use case here: https://goo.gl/RKTeoc', - ); - - const projectAssets = getAssets(context.root); - const dependencies = getProjectDependencies(context.root); - const dependenciesConfig = dependencies.map(dependency => - getDependencyConfig(context, platforms, dependency), - ); +function linkAll(config: ConfigT) { + const projectAssets = config.assets; const assets = dedupeAssets( - dependenciesConfig.reduce( - (acc, dependency) => acc.concat(dependency.assets), + Object.keys(config.dependencies).reduce( + (acc, dependency) => acc.concat(config.dependencies[dependency].assets), projectAssets, ), ); - const tasks = flatten( - dependenciesConfig.map(config => [ - () => promisify(config.commands.prelink || commandStub), - () => linkDependency(platforms, project, config), - () => promisify(config.commands.postlink || commandStub), - () => linkAssets(platforms, project, assets), - ]), + const tasks = flatMap( + config.dependencies, + dependency => [ + () => promisify(dependency.hooks.prelink || commandStub), + () => linkDependency(config.platforms, config.project, dependency), + () => promisify(dependency.hooks.postlink || commandStub), + ], + () => linkAssets(config.platforms, config.project, assets), ); return promiseWaterfall(tasks).catch(err => { - logger.error( - `Something went wrong while linking. Error: ${err.message} \n` + - 'Please file an issue here: https://github.com/react-native-community/react-native-cli/issues', + throw new CLIError( + `Something went wrong while linking. Reason: ${err.message}`, + err, ); - throw err; }); } diff --git a/packages/cli/src/commands/link/linkAssets.js b/packages/cli/src/commands/link/linkAssets.js index 7060ef4ca..462fbc676 100644 --- a/packages/cli/src/commands/link/linkAssets.js +++ b/packages/cli/src/commands/link/linkAssets.js @@ -1,7 +1,7 @@ // @flow import {isEmpty} from 'lodash'; -import type {PlatformsT, ProjectConfigT} from '../../tools/types.flow'; +import type {PlatformsT, ProjectConfigT} from '../../tools/config/types.flow'; import logger from '../../tools/logger'; diff --git a/packages/cli/src/commands/link/linkDependency.js b/packages/cli/src/commands/link/linkDependency.js index e669aae29..23b2fbb04 100644 --- a/packages/cli/src/commands/link/linkDependency.js +++ b/packages/cli/src/commands/link/linkDependency.js @@ -1,26 +1,29 @@ // @flow import type { - PlatformsT, + DependencyConfigT, ProjectConfigT, - DependenciesConfig, -} from '../../tools/types.flow'; + PlatformsT, +} from '../../tools/config/types.flow'; import logger from '../../tools/logger'; import pollParams from './pollParams'; -import {getPlatformName} from '../../tools/getPlatforms'; +import getPlatformName from './getPlatformName'; const linkDependency = async ( platforms: PlatformsT, project: ProjectConfigT, - dependency: DependenciesConfig, + dependency: DependencyConfigT, ) => { const params = await pollParams(dependency.params); Object.keys(platforms || {}).forEach(platform => { - if (!project[platform] || !dependency.config[platform]) { + const projectConfig = project[platform]; + const dependencyConfig = dependency.platforms[platform]; + + if (!projectConfig || !dependencyConfig) { return; } - + const {name} = dependency; const linkConfig = platforms[platform] && platforms[platform].linkConfig && @@ -31,31 +34,21 @@ const linkDependency = async ( } const isInstalled = linkConfig.isInstalled( - project[platform], - dependency.name, - dependency.config[platform], + projectConfig, + name, + dependencyConfig, ); if (isInstalled) { logger.info( - `${getPlatformName(platform)} module "${ - dependency.name - }" is already linked`, + `${getPlatformName(platform)} module "${name}" is already linked`, ); return; } - logger.info( - `Linking "${dependency.name}" ${getPlatformName(platform)} dependency`, - ); + logger.info(`Linking "${name}" ${getPlatformName(platform)} dependency`); - linkConfig.register( - dependency.name, - dependency.config[platform] || {}, - params, - // $FlowFixMe: We check if project[platform] exists on line 42 - project[platform], - ); + linkConfig.register(name, dependencyConfig, params, projectConfig); logger.info( `${getPlatformName(platform)} module "${ diff --git a/packages/cli/src/commands/link/unlink.js b/packages/cli/src/commands/link/unlink.js index 72f44e8c5..67ace63a1 100644 --- a/packages/cli/src/commands/link/unlink.js +++ b/packages/cli/src/commands/link/unlink.js @@ -7,16 +7,14 @@ * @flow */ -import {flatten, isEmpty, difference} from 'lodash'; +import {flatMap, values, difference} from 'lodash'; import type {ContextT} from '../../tools/types.flow'; +import dedent from 'dedent'; import logger from '../../tools/logger'; -import getProjectConfig from './getProjectConfig'; -import getDependencyConfig from './getDependencyConfig'; -import getProjectDependencies from './getProjectDependencies'; import promiseWaterfall from './promiseWaterfall'; import commandStub from './commandStub'; import promisify from './promisify'; -import getPlatforms, {getPlatformName} from '../../tools/getPlatforms'; +import getPlatformName from './getPlatformName'; const unlinkDependency = ( platforms, @@ -26,7 +24,9 @@ const unlinkDependency = ( otherDependencies, ) => { Object.keys(platforms || {}).forEach(platform => { - if (!project[platform] || !dependency.config[platform]) { + const projectConfig = project[platform]; + const dependencyConfig = dependency.platforms[platform]; + if (!projectConfig || !dependencyConfig) { return; } @@ -40,9 +40,9 @@ const unlinkDependency = ( } const isInstalled = linkConfig.isInstalled( - project[platform], + projectConfig, packageName, - dependency.config[platform], + dependencyConfig, ); if (!isInstalled) { @@ -58,10 +58,8 @@ const unlinkDependency = ( linkConfig.unregister( packageName, - // $FlowFixMe: We check for existence on line 38 - dependency.config[platform], - // $FlowFixMe: We check for existence on line 38 - project[platform], + dependencyConfig, + projectConfig, otherDependencies, ); @@ -82,76 +80,55 @@ const unlinkDependency = ( function unlink(args: Array, ctx: ContextT) { const packageName = args[0]; - let platforms; + const {[packageName]: dependency, ...otherDependencies} = ctx.dependencies; - try { - platforms = getPlatforms(ctx.root); - } catch (err) { - logger.error( - "No package.json found. Are you sure it's a React Native project?", - ); - return Promise.reject(err); - } - - const allDependencies = getProjectDependencies(ctx.root).map(dependency => - getDependencyConfig(ctx, platforms, dependency), - ); - let otherDependencies; - let dependency; - - try { - const idx = allDependencies.findIndex(p => p.name === packageName); - - if (idx === -1) { - throw new Error(`Project "${packageName}" is not a react-native library`); - } - - otherDependencies = [...allDependencies]; - dependency = otherDependencies.splice(idx, 1)[0]; - } catch (err) { - return Promise.reject(err); + if (!dependency) { + throw new Error(dedent` + Failed to unlink "${packageName}". It appears that the project is not linked yet. + `); } - const project = getProjectConfig(ctx, platforms); + const dependencies = values(otherDependencies); const tasks = [ - () => promisify(dependency.commands.preunlink || commandStub), + () => promisify(dependency.hooks.preulink || commandStub), () => unlinkDependency( - platforms, - project, + ctx.platforms, + ctx.project, dependency, packageName, - otherDependencies, + dependencies, ), - () => promisify(dependency.commands.postunlink || commandStub), + () => promisify(dependency.hooks.postunlink || commandStub), ]; return promiseWaterfall(tasks) .then(() => { - // @todo move all these to `tasks` array, just like in - // link + // @todo move all these to `tasks` array + // @todo it is possible we could be unlinking some project assets in case of duplicate const assets = difference( dependency.assets, - flatten(allDependencies, d => d.assets), + flatMap(dependencies, d => d.assets), ); - if (isEmpty(assets)) { + if (assets.length === 0) { return; } - Object.keys(platforms || {}).forEach(platform => { + Object.keys(ctx.platforms || {}).forEach(platform => { + const projectConfig = ctx.project[platform]; const linkConfig = - platforms[platform] && - platforms[platform].linkConfig && - platforms[platform].linkConfig(); - if (!linkConfig || !linkConfig.unlinkAssets || !project[platform]) { + ctx.platforms[platform] && + ctx.platforms[platform].linkConfig && + ctx.platforms[platform].linkConfig(); + if (!linkConfig || !linkConfig.unlinkAssets || !projectConfig) { return; } logger.info(`Unlinking assets from ${platform} project`); - // $FlowFixMe: We check for platorm existence on line 150 - linkConfig.unlinkAssets(assets, project[platform]); + + linkConfig.unlinkAssets(assets, projectConfig); }); logger.info( diff --git a/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js b/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js index 5b550fdf2..4e91f187d 100644 --- a/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js +++ b/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js @@ -7,6 +7,7 @@ import stripAnsi from 'strip-ansi'; import upgrade from '../upgrade'; import {fetch} from '../../../tools/fetch'; import logger from '../../../tools/logger'; +import loadConfig from '../../../tools/config'; jest.mock('https'); jest.mock('fs'); @@ -52,17 +53,11 @@ jest.mock('../../../tools/logger', () => ({ const currentVersion = '0.57.8'; const newVersion = '0.58.4'; const olderVersion = '0.56.0'; -const ctx = { - root: '/project/root', - reactNativePath: '', - commands: [], - platforms: {}, - dependencies: {}, - haste: { - providesModuleNodeModules: [], - platforms: [], - }, -}; + +jest.mock('../../../tools/config'); + +const ctx = loadConfig(); + const opts = { legacy: false, }; diff --git a/packages/cli/src/tools/__tests__/findPlugins-test.js b/packages/cli/src/tools/__tests__/findPlugins-test.js deleted file mode 100644 index c1cee2079..000000000 --- a/packages/cli/src/tools/__tests__/findPlugins-test.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @emails oncall+javascript_foundation - */ - -import findPlugins from '../findPlugins'; - -const path = require('path'); - -const ROOT = path.join(__dirname, '..', '..'); -const pjsonPath = path.join(ROOT, './package.json'); - -describe('findPlugins', () => { - beforeEach(() => { - jest.resetModules(); - }); - - it('returns an array of dependencies', () => { - jest.doMock( - pjsonPath, - () => ({ - dependencies: {'rnpm-plugin-test': '*'}, - }), - {virtual: true}, - ); - - expect(findPlugins(ROOT)).toHaveProperty('commands'); - expect(findPlugins(ROOT)).toHaveProperty('platforms'); - expect(findPlugins(ROOT).commands).toHaveLength(1); - expect(findPlugins(ROOT).commands[0]).toBe('rnpm-plugin-test'); - expect(findPlugins(ROOT).platforms).toHaveLength(0); - }); - - it('returns an empty array if there are no plugins in this folder', () => { - jest.doMock(pjsonPath, () => ({}), {virtual: true}); - expect(findPlugins(ROOT)).toHaveProperty('commands'); - expect(findPlugins(ROOT)).toHaveProperty('platforms'); - expect(findPlugins(ROOT).commands).toHaveLength(0); - expect(findPlugins(ROOT).platforms).toHaveLength(0); - }); - - it('returns an object with empty arrays if there is no package.json in the supplied folder', () => { - expect(findPlugins('fake-path')).toHaveProperty('commands'); - expect(findPlugins('fake-path')).toHaveProperty('platforms'); - expect(findPlugins('fake-path').commands).toHaveLength(0); - expect(findPlugins('fake-path').platforms).toHaveLength(0); - }); - - it('returns plugins from both dependencies and dev dependencies', () => { - jest.doMock( - pjsonPath, - () => ({ - dependencies: {'rnpm-plugin-test': '*'}, - devDependencies: {'rnpm-plugin-test-2': '*'}, - }), - {virtual: true}, - ); - expect(findPlugins(ROOT)).toHaveProperty('commands'); - expect(findPlugins(ROOT)).toHaveProperty('platforms'); - expect(findPlugins(ROOT).commands).toHaveLength(2); - expect(findPlugins(ROOT).platforms).toHaveLength(0); - }); - - it('returns unique list of plugins', () => { - jest.doMock( - pjsonPath, - () => ({ - dependencies: {'rnpm-plugin-test': '*'}, - devDependencies: {'rnpm-plugin-test': '*'}, - }), - {virtual: true}, - ); - expect(findPlugins(ROOT).commands).toHaveLength(1); - }); - - it('returns plugins in scoped modules', () => { - jest.doMock( - pjsonPath, - () => ({ - dependencies: { - '@org/rnpm-plugin-test': '*', - '@org/react-native-test': '*', - '@react-native/test': '*', - '@react-native-org/test': '*', - }, - }), - {virtual: true}, - ); - - expect(findPlugins(ROOT)).toHaveProperty('commands'); - expect(findPlugins(ROOT)).toHaveProperty('platforms'); - expect(findPlugins(ROOT).commands[0]).toBe('@org/rnpm-plugin-test'); - }); -}); diff --git a/packages/cli/src/tools/assign.js b/packages/cli/src/tools/assign.js new file mode 100644 index 000000000..18f061712 --- /dev/null +++ b/packages/cli/src/tools/assign.js @@ -0,0 +1,25 @@ +/** + * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign + * + * Similar to Object.assign(), but it doesn't execute getters. This allows us to have + * lazy properties on an object and still be able to merge them together + * + * @flow + */ +export default function assign(target: Object, ...sources: Object[]) { + sources.forEach(source => { + let descriptors = Object.keys(source).reduce((acc, key) => { + acc[key] = Object.getOwnPropertyDescriptor(source, key); + return acc; + }, {}); + // by default, Object.assign copies enumerable Symbols too + Object.getOwnPropertySymbols(source).forEach(sym => { + let descriptor = Object.getOwnPropertyDescriptor(source, sym); + if (descriptor && descriptor.enumerable) { + descriptors[sym.toString()] = descriptor; + } + }); + Object.defineProperties(target, descriptors); + }); + return target; +} diff --git a/packages/cli/src/tools/config/__mocks__/index.js b/packages/cli/src/tools/config/__mocks__/index.js new file mode 100644 index 000000000..78f27628a --- /dev/null +++ b/packages/cli/src/tools/config/__mocks__/index.js @@ -0,0 +1,25 @@ +/** + * @flow + */ + +export default function mockedLoadConfig() { + return { + root: '/project/root', + reactNativePath: '', + commands: [], + platforms: { + ios: {projectConfig: () => null, dependencyConfig: () => null}, + android: {projectConfig: () => null, dependencyConfig: () => null}, + }, + project: { + ios: null, + android: null, + }, + dependencies: {}, + assets: [], + haste: { + providesModuleNodeModules: [], + platforms: [], + }, + }; +} diff --git a/packages/cli/src/tools/config/__tests__/__snapshots__/index.js.snap b/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.js.snap similarity index 78% rename from packages/cli/src/tools/config/__tests__/__snapshots__/index.js.snap rename to packages/cli/src/tools/config/__tests__/__snapshots__/index-test.js.snap index aaa2e0cb7..3fc391a4b 100644 --- a/packages/cli/src/tools/config/__tests__/__snapshots__/index.js.snap +++ b/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.js.snap @@ -1,12 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should deep merge project configuration with default values 1`] = ` +exports[`should merge project configuration with default values 1`] = ` Object { + "assets": Array [], "commands": Array [], "dependencies": Object { - "react-native-test": Object { + "react-native": Object { "assets": Array [], "hooks": Object {}, + "name": "react-native", + "params": Array [], + "platforms": Object { + "android": null, + "ios": null, + }, + }, + "react-native-test": Object { + "assets": Array [ + "foo", + ], + "hooks": Object {}, + "name": "react-native-test", "params": Array [], "platforms": Object { "android": null, @@ -26,13 +40,22 @@ Object { }, }, "haste": Object { - "platforms": Array [], - "providesModuleNodeModules": Array [], + "platforms": Array [ + "ios", + "android", + ], + "providesModuleNodeModules": Array [ + "react-native", + ], }, "platforms": Object { "android": Object {}, "ios": Object {}, }, + "project": Object { + "android": null, + "ios": null, + }, "reactNativePath": ".", "root": "<>", } @@ -40,16 +63,15 @@ Object { exports[`should have a valid structure by default 1`] = ` Object { + "assets": Array [], "commands": Array [], "dependencies": Object {}, "haste": Object { "platforms": Array [], "providesModuleNodeModules": Array [], }, - "platforms": Object { - "android": Object {}, - "ios": Object {}, - }, + "platforms": Object {}, + "project": Object {}, "reactNativePath": ".", "root": "<>", } @@ -66,8 +88,6 @@ Object { ], }, "platforms": Object { - "android": Object {}, - "ios": Object {}, "windows": Object {}, }, } @@ -84,6 +104,7 @@ exports[`should read \`rnpm\` config from a dependency and transform it to a new Object { "assets": Array [], "hooks": Object {}, + "name": "react-native-foo", "params": Array [], "platforms": Object { "android": null, @@ -107,6 +128,7 @@ exports[`should read a config of a dependency and use it to load other settings Object { "assets": Array [], "hooks": Object {}, + "name": "react-native-test", "params": Array [], "platforms": Object { "android": null, @@ -128,9 +150,20 @@ Object { exports[`should return dependencies from package.json 1`] = ` Object { + "react-native": Object { + "assets": Array [], + "hooks": Object {}, + "name": "react-native", + "params": Array [], + "platforms": Object { + "android": null, + "ios": null, + }, + }, "react-native-test": Object { "assets": Array [], "hooks": Object {}, + "name": "react-native-test", "params": Array [], "platforms": Object { "android": null, @@ -150,3 +183,15 @@ Object { }, } `; + +exports[`should automatically put "react-native" into haste config 1`] = ` +Object { + "platforms": Array [ + "ios", + "android", + ], + "providesModuleNodeModules": Array [ + "react-native", + ], +} +`; diff --git a/packages/cli/src/tools/__tests__/findAssets-test.js b/packages/cli/src/tools/config/__tests__/findAssets-test.js similarity index 90% rename from packages/cli/src/tools/__tests__/findAssets-test.js rename to packages/cli/src/tools/config/__tests__/findAssets-test.js index a912a846f..ed0358854 100644 --- a/packages/cli/src/tools/__tests__/findAssets-test.js +++ b/packages/cli/src/tools/config/__tests__/findAssets-test.js @@ -8,15 +8,14 @@ * @emails oncall+javascript_foundation */ -import dependencies from '../__fixtures__/dependencies'; +import dependencies from '../../__fixtures__/dependencies'; +import findAssets from '../findAssets'; jest.mock('path'); jest.mock('fs'); const fs = require('fs'); -const {findAssets} = require('../getAssets'); - describe('findAssets', () => { beforeEach(() => { fs.__setMockFilesystem({testDir: dependencies.withAssets}); diff --git a/packages/cli/src/tools/config/__tests__/findDependencies-test.js b/packages/cli/src/tools/config/__tests__/findDependencies-test.js new file mode 100644 index 000000000..92750a82e --- /dev/null +++ b/packages/cli/src/tools/config/__tests__/findDependencies-test.js @@ -0,0 +1,32 @@ +/** + * @flow + */ + +import findDependencies from '../findDependencies'; + +import { + cleanup, + writeFiles, + getTempDirectory, +} from '../../../../../../e2e/helpers'; + +beforeEach(() => { + cleanup(DIR); + jest.resetModules(); +}); + +afterEach(() => cleanup(DIR)); + +const DIR = getTempDirectory('find_dependencies_test'); + +test('returns plugins from both dependencies and dev dependencies', () => { + writeFiles(DIR, { + 'package.json': ` + { + "dependencies": {"rnpm-plugin-test": "*"}, + "devDependencies": {"rnpm-plugin-test-2": "*"} + } + `, + }); + expect(findDependencies(DIR)).toHaveLength(2); +}); diff --git a/packages/cli/src/tools/config/__tests__/index.js b/packages/cli/src/tools/config/__tests__/index-test.js similarity index 81% rename from packages/cli/src/tools/config/__tests__/index.js rename to packages/cli/src/tools/config/__tests__/index-test.js index be2173b51..c5ddc765a 100644 --- a/packages/cli/src/tools/config/__tests__/index.js +++ b/packages/cli/src/tools/config/__tests__/index-test.js @@ -2,7 +2,7 @@ * @flow */ -import loadConfig from '../'; +import loadConfig from '..'; import { cleanup, @@ -39,11 +39,13 @@ test('should have a valid structure by default', () => { test('should return dependencies from package.json', () => { writeFiles(DIR, { + 'node_modules/react-native/package.json': '{}', 'node_modules/react-native-test/package.json': '{}', 'node_modules/react-native-test/ios/HelloWorld.xcodeproj/project.pbxproj': '', 'package.json': `{ "dependencies": { + "react-native": "0.0.1", "react-native-test": "0.0.1" }, "react-native": { @@ -57,6 +59,7 @@ test('should return dependencies from package.json', () => { test('should read a config of a dependency and use it to load other settings', () => { writeFiles(DIR, { + 'node_modules/react-native/package.json': '{}', 'node_modules/react-native-test/package.json': `{ "react-native": { "dependency": { @@ -70,6 +73,7 @@ test('should read a config of a dependency and use it to load other settings', ( }`, 'package.json': `{ "dependencies": { + "react-native": "0.0.1", "react-native-test": "0.0.1" }, "react-native": { @@ -83,13 +87,21 @@ test('should read a config of a dependency and use it to load other settings', ( ).toMatchSnapshot(); }); -test('should deep merge project configuration with default values', () => { +test('should merge project configuration with default values', () => { writeFiles(DIR, { - 'node_modules/react-native-test/package.json': '{}', + 'node_modules/react-native/package.json': '{}', + 'node_modules/react-native-test/package.json': `{ + "react-native": { + "dependency": { + "assets": ["foo", "baz"] + } + } + }`, 'node_modules/react-native-test/ios/HelloWorld.xcodeproj/project.pbxproj': '', 'package.json': `{ "dependencies": { + "react-native": "0.0.1", "react-native-test": "0.0.1" }, "react-native": { @@ -100,7 +112,8 @@ test('should deep merge project configuration with default values', () => { "ios": { "sourceDir": "./abc" } - } + }, + "assets": ["foo"] } } } @@ -112,6 +125,7 @@ test('should deep merge project configuration with default values', () => { test('should read `rnpm` config from a dependency and transform it to a new format', () => { writeFiles(DIR, { + 'node_modules/react-native/package.json': '{}', 'node_modules/react-native-foo/package.json': `{ "name": "react-native-foo", "rnpm": { @@ -122,6 +136,7 @@ test('should read `rnpm` config from a dependency and transform it to a new form }`, 'package.json': `{ "dependencies": { + "react-native": "0.0.1", "react-native-foo": "0.0.1" }, "react-native": { @@ -195,3 +210,19 @@ test('should load an out-of-tree "windows" platform that ships with a dependency const {haste, platforms} = loadConfig(DIR); expect(removeString({haste, platforms}, DIR)).toMatchSnapshot(); }); + +test('should automatically put "react-native" into haste config', () => { + writeFiles(DIR, { + 'node_modules/react-native/package.json': '{}', + 'package.json': `{ + "dependencies": { + "react-native": "0.0.1" + }, + "react-native": { + "reactNativePath": "." + } + }`, + }); + const {haste} = loadConfig(DIR); + expect(haste).toMatchSnapshot(); +}); diff --git a/packages/cli/src/tools/__tests__/makeCommand-test.js b/packages/cli/src/tools/config/__tests__/makeHook-test.js similarity index 56% rename from packages/cli/src/tools/__tests__/makeCommand-test.js rename to packages/cli/src/tools/config/__tests__/makeHook-test.js index 6a058a1bb..d16953c4a 100644 --- a/packages/cli/src/tools/__tests__/makeCommand-test.js +++ b/packages/cli/src/tools/config/__tests__/makeHook-test.js @@ -1,12 +1,7 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @emails oncall+javascript_foundation + * @flow */ +import makeHook from '../makeHook'; let spawnError = false; @@ -16,26 +11,24 @@ jest.setMock('child_process', { }), }); -const {makeCommand} = require('../getHooks'); - afterAll(() => { jest.restoreAllMocks(); }); -describe('makeCommand', () => { - const command = makeCommand('echo'); +describe('makeHook', () => { + const hook = makeHook('echo'); it('generates a function around shell command', () => { - expect(typeof command).toBe('function'); + expect(typeof hook).toBe('function'); }); it('throws an error if there is no callback provided', () => { - expect(command).toThrow(); + expect(hook).toThrow(); }); it('invokes a callback after command execution', () => { const spy = jest.fn(); - command(spy); + hook(spy); expect(spy.mock.calls).toHaveLength(1); }); @@ -43,7 +36,7 @@ describe('makeCommand', () => { spawnError = true; const cb = jest.fn(); expect(() => { - command(cb); + hook(cb); }).toThrow(); }); }); diff --git a/packages/cli/src/tools/getAssets.js b/packages/cli/src/tools/config/findAssets.js similarity index 61% rename from packages/cli/src/tools/getAssets.js rename to packages/cli/src/tools/config/findAssets.js index d0927ea61..9f35e5c0c 100644 --- a/packages/cli/src/tools/getAssets.js +++ b/packages/cli/src/tools/config/findAssets.js @@ -4,7 +4,6 @@ import glob from 'glob'; import path from 'path'; -import getPackageConfiguration from './getPackageConfiguration'; const findAssetsInFolder = folder => glob.sync(path.join(folder, '**'), {nodir: true}); @@ -15,7 +14,7 @@ const findAssetsInFolder = folder => * * It returns an array of absolute paths to files found. */ -export function findAssets(folder: string, assets?: string[]) { +export default function findAssets(folder: string, assets: string[]) { return (assets || []) .map(asset => path.join(folder, asset)) .reduce( @@ -24,11 +23,3 @@ export function findAssets(folder: string, assets?: string[]) { [], ); } - -/** - * Returns a project configuration in a given folder - */ -export default function getAssets(root: string) { - const config = getPackageConfiguration(root); - return findAssets(root, config.assets); -} diff --git a/packages/cli/src/tools/config/findDependencies.js b/packages/cli/src/tools/config/findDependencies.js index 7fda35955..c5f392964 100644 --- a/packages/cli/src/tools/config/findDependencies.js +++ b/packages/cli/src/tools/config/findDependencies.js @@ -3,24 +3,18 @@ */ import path from 'path'; - -const pluginRe = new RegExp( - [ - '^react-native-', - '^@(.*)/react-native-', - '^@react-native(.*)/(?!rnpm-plugin-)', - ].join('|'), -); +import fs from 'fs'; /** - * Returns an array of dependencies from project's package.json that - * are likely to be React Native packages (see regular expression above) + * Returns an array of dependencies from project's package.json */ export default function findDependencies(root: string): Array { let pjson; try { - pjson = require(path.join(root, 'package.json')); + pjson = JSON.parse( + fs.readFileSync(path.join(root, 'package.json'), 'UTF-8'), + ); } catch (e) { return []; } @@ -30,5 +24,5 @@ export default function findDependencies(root: string): Array { ...Object.keys(pjson.devDependencies || {}), ]; - return deps.filter(dependency => pluginRe.test(dependency)); + return deps; } diff --git a/packages/cli/src/tools/config/index.js b/packages/cli/src/tools/config/index.js index 82636944f..101a1ca15 100644 --- a/packages/cli/src/tools/config/index.js +++ b/packages/cli/src/tools/config/index.js @@ -1,63 +1,88 @@ /** * @flow */ -import dedent from 'dedent'; import path from 'path'; -import merge from 'deepmerge'; +import deepmerge from 'deepmerge'; +import {mapValues} from 'lodash'; import findDependencies from './findDependencies'; +import resolveReactNativePath from './resolveReactNativePath'; +import findAssets from './findAssets'; +import makeHook from './makeHook'; import { - readProjectConfigFromDisk, + readConfigFromDisk, readDependencyConfigFromDisk, readLegacyDependencyConfigFromDisk, } from './readConfigFromDisk'; -import {type ProjectConfigT, type RawProjectConfigT} from './types.flow'; +import {type ConfigT} from './types.flow'; + +import assign from '../assign'; /** * Built-in platforms */ import * as ios from '../ios'; import * as android from '../android'; -import resolveReactNativePath from './resolveReactNativePath'; + +/** + * `deepmerge` concatenates arrays by default instead of overwriting them. + * We define custom merging function for arrays to change that behaviour + */ +const merge = (...objs: Object[]) => + deepmerge(...objs, { + arrayMerge: (destinationArray, sourceArray, options) => sourceArray, + }); /** * Loads CLI configuration */ -function loadConfig(projectRoot: string = process.cwd()): ProjectConfigT { - const inferredProjectConfig = findDependencies(projectRoot).reduce( - (acc: RawProjectConfigT, dependencyName) => { +function loadConfig(projectRoot: string = process.cwd()): ConfigT { + const userConfig = readConfigFromDisk(projectRoot); + + const finalConfig = findDependencies(projectRoot).reduce( + (acc: ConfigT, dependencyName) => { const root = path.join(projectRoot, 'node_modules', dependencyName); const config = readLegacyDependencyConfigFromDisk(root) || readDependencyConfigFromDisk(root); - return { - ...acc, - dependencies: { - ...acc.dependencies, - // $FlowIssue: Computed getters are not yet supported. + // @todo: Move this to React Native in the future + if (dependencyName === 'react-native') { + config.platforms = {ios, android}; + } + + const isPlatform = Object.keys(config.platforms).length > 0; + + return assign({}, acc, { + dependencies: assign({}, acc.dependencies, { + // $FlowExpectedError: Dynamic getters are not supported get [dependencyName]() { - return { - platforms: Object.keys(acc.platforms).reduce( - (dependency, platform) => { - dependency[platform] = acc.platforms[ - platform - ].dependencyConfig( - root, - config.dependency.platforms[platform], - ); - return dependency; - }, - {}, - ), - assets: config.dependency.assets, - hooks: config.dependency.hooks, - params: config.dependency.params, - }; + return merge( + { + name: dependencyName, + platforms: Object.keys(finalConfig.platforms).reduce( + (dependency, platform) => { + // Linking platforms is not supported + dependency[platform] = isPlatform + ? null + : finalConfig.platforms[platform].dependencyConfig( + root, + config.dependency.platforms[platform] || {}, + ); + return dependency; + }, + {}, + ), + assets: findAssets(root, config.dependency.assets), + hooks: mapValues(config.dependency.hooks, makeHook), + params: config.dependency.params, + }, + userConfig.dependencies[dependencyName] || {}, + ); }, - }, + }), commands: acc.commands.concat( config.commands.map(pathToCommand => path.join(dependencyName, pathToCommand), @@ -69,50 +94,43 @@ function loadConfig(projectRoot: string = process.cwd()): ProjectConfigT { }, haste: { providesModuleNodeModules: acc.haste.providesModuleNodeModules.concat( - Object.keys(config.platforms).length > 0 ? dependencyName : [], + isPlatform ? dependencyName : [], ), platforms: [...acc.haste.platforms, ...Object.keys(config.platforms)], }, - }; + }); }, ({ root: projectRoot, - reactNativePath: resolveReactNativePath(projectRoot), + get reactNativePath() { + return ( + userConfig.reactNativePath || resolveReactNativePath(projectRoot) + ); + }, dependencies: {}, - commands: [], - platforms: { - ios, - android, + commands: userConfig.commands, + get assets() { + return findAssets(projectRoot, userConfig.assets); }, + platforms: {}, haste: { providesModuleNodeModules: [], platforms: [], }, - }: RawProjectConfigT), - ); - - const config: RawProjectConfigT = merge( - inferredProjectConfig, - readProjectConfigFromDisk(projectRoot), - ); - - if (config.reactNativePath === null && !process.argv.includes('init')) { - throw new Error(dedent` - Unable to find React Native files. Make sure "react-native" module is installed - in your project dependencies. - - If you are using React Native from a non-standard location, consider setting: - { - "react-native": { - "reactNativePath": "./path/to/react-native" + get project() { + const project = {}; + for (const platform in finalConfig.platforms) { + project[platform] = finalConfig.platforms[platform].projectConfig( + projectRoot, + userConfig.project[platform] || {}, + ); } - } - in your \`package.json\`. - `); - } + return project; + }, + }: ConfigT), + ); - // $FlowIssue: `reactNativePath: null` is never null at this point - return config; + return finalConfig; } export default loadConfig; diff --git a/packages/cli/src/tools/getHooks.js b/packages/cli/src/tools/config/makeHook.js similarity index 60% rename from packages/cli/src/tools/getHooks.js rename to packages/cli/src/tools/config/makeHook.js index aac342808..2eaad64db 100644 --- a/packages/cli/src/tools/getHooks.js +++ b/packages/cli/src/tools/config/makeHook.js @@ -3,9 +3,8 @@ */ import {spawn} from 'child_process'; -import getPackageConfiguration from './getPackageConfiguration'; -export function makeCommand(command: string) { +export default function makeCommand(command: string) { return (cb: Function) => { if (!cb) { throw new Error( @@ -30,15 +29,3 @@ export function makeCommand(command: string) { }); }; } - -export default function getHooks(root: string) { - const commands = getPackageConfiguration(root).commands || {}; - - const acc = {}; - - Object.keys(commands).forEach(command => { - acc[command] = makeCommand(commands[command]); - }); - - return acc; -} diff --git a/packages/cli/src/tools/config/readConfigFromDisk.js b/packages/cli/src/tools/config/readConfigFromDisk.js index 11ebc7915..90aad6cf5 100644 --- a/packages/cli/src/tools/config/readConfigFromDisk.js +++ b/packages/cli/src/tools/config/readConfigFromDisk.js @@ -7,7 +7,7 @@ import Joi from 'joi'; import cosmiconfig from 'cosmiconfig'; import path from 'path'; -import {type DependencyConfigT, type ProjectConfigT} from './types.flow'; +import {type UserDependencyConfigT, type UserConfigT} from './types.flow'; import {JoiError} from '../errors'; @@ -23,7 +23,7 @@ const searchPlaces = ['react-native.config.js', 'package.json']; * Reads a project configuration as defined by the user in the current * workspace. */ -export function readProjectConfigFromDisk(rootFolder: string): ProjectConfigT { +export function readConfigFromDisk(rootFolder: string): UserConfigT { const explorer = cosmiconfig('react-native', {searchPlaces}); const {config} = explorer.searchSync(rootFolder) || {config: undefined}; @@ -43,7 +43,7 @@ export function readProjectConfigFromDisk(rootFolder: string): ProjectConfigT { */ export function readDependencyConfigFromDisk( rootFolder: string, -): DependencyConfigT { +): UserDependencyConfigT { const explorer = cosmiconfig('react-native', { stopDir: rootFolder, searchPlaces, @@ -65,7 +65,7 @@ export function readDependencyConfigFromDisk( */ export function readLegacyDependencyConfigFromDisk( rootFolder: string, -): ?DependencyConfigT { +): ?UserDependencyConfigT { const {rnpm: config, name} = require(path.join(rootFolder, 'package.json')); if (!config) { diff --git a/packages/cli/src/tools/config/resolveReactNativePath.js b/packages/cli/src/tools/config/resolveReactNativePath.js index 422a10249..bf1facd66 100644 --- a/packages/cli/src/tools/config/resolveReactNativePath.js +++ b/packages/cli/src/tools/config/resolveReactNativePath.js @@ -2,6 +2,7 @@ * @flow */ import path from 'path'; +import dedent from 'dedent'; /** * Finds path to React Native inside `node_modules` or throws @@ -16,6 +17,17 @@ export default function resolveReactNativePath(root: string) { }), ); } catch (_ignored) { - return null; + throw new Error(dedent` + Unable to find React Native files. Make sure "react-native" module is installed + in your project dependencies. + + If you are using React Native from a non-standard location, consider setting: + { + "react-native": { + "reactNativePath": "./path/to/react-native" + } + } + in your \`package.json\`. + `); } } diff --git a/packages/cli/src/tools/config/schema.js b/packages/cli/src/tools/config/schema.js index 93f058cb9..067395c52 100644 --- a/packages/cli/src/tools/config/schema.js +++ b/packages/cli/src/tools/config/schema.js @@ -10,7 +10,7 @@ const map = (key, value) => .pattern(key, value); /** - * Schema for DependencyConfigT + * Schema for UserDependencyConfigT */ export const dependencyConfig = t .object({ @@ -39,7 +39,7 @@ export const dependencyConfig = t .array() .items(t.string()) .default([]), - hooks: map(t.string(), t.string()).default(), + hooks: map(t.string(), t.string()).default({}), params: t .array() .items( @@ -110,13 +110,39 @@ export const projectConfig = t ), }) .allow(null), - ), - commands: t.array().items(t.string()), - haste: t.object({ - providesModuleNodeModules: t.array().items(t.string()), - platforms: t.array().items(t.string()), - }), + ).default({}), reactNativePath: t.string(), - root: t.string(), + project: map(t.string(), t.any()) + .keys({ + ios: t + .object({ + project: t.string(), + sharedLibraries: t.array().items(t.string()), + libraryFolder: t.string(), + }) + .default({}), + android: t + .object({ + sourceDir: t.string(), + manifestPath: t.string(), + packageName: t.string(), + packageFolder: t.string(), + mainFilePath: t.string(), + stringsPath: t.string(), + settingsGradlePath: t.string(), + assetsPath: t.string(), + buildGradlePath: t.string(), + }) + .default({}), + }) + .default(), + assets: t + .array() + .items(t.string()) + .default([]), + commands: t + .array() + .items(t.string()) + .default([]), }) - .default({}); + .default(); diff --git a/packages/cli/src/tools/config/types.flow.js b/packages/cli/src/tools/config/types.flow.js index 01901b934..9e038d86b 100644 --- a/packages/cli/src/tools/config/types.flow.js +++ b/packages/cli/src/tools/config/types.flow.js @@ -2,75 +2,193 @@ * @flow */ -import type { - AndroidConfigParamsT, - IOSConfigParamsT, - InquirerPromptT, - DependencyConfigAndroidT, - DependencyConfigIOST, -} from '../types.flow'; +/** + * Opaque type that describes the Inquirer question format. Not typed, since we just + * pass it directly to Inquirer. Validation is done with Joi in `schema.js` + */ +type InquirerPromptT = any; /** - * A map of hooks to run pre/post some of the CLI actions + * Settings that a library author can define in the configuration bundled with + * dependency for Android + * + * See UserDependencyConfigT for details */ -type HooksT = { - [key: string]: string, - prelink?: string, - postlink?: string, +type DependencyParamsAndroidT = { + sourceDir?: string, + manifestPath?: string, + packageImportPath?: string, + packageInstance?: string, }; /** - * A map with additional platforms that ship with a dependency. + * Settings that user can define in the project configuration for Android + * + * See UserConfigT for details */ -export type PlatformsT = { - [key: string]: { - dependencyConfig?: Function, - projectConfig?: Function, - linkConfig?: Function, - }, +type ProjectParamsAndroidT = { + sourceDir?: string, + manifestPath?: string, + packageName?: string, + packageFolder?: string, + mainFilePath?: string, + stringsPath?: string, + settingsGradlePath?: string, + assetsPath?: string, + buildGradlePath?: string, }; -export type DependencyConfigT = { - dependency: { - platforms: { - android?: AndroidConfigParamsT, - ios?: IOSConfigParamsT, - [key: string]: any, - }, - assets: string[], - hooks: HooksT, - params: InquirerPromptT[], +/** + * Settings that user can define in the project configuration for iOS. + * Same for dependency - we share the type. + * + * See UserDependencyConfigT and UserConfigT for details + */ +type ProjectParamsIOST = { + project?: string, + sharedLibraries?: string[], + libraryFolder?: string, +}; + +type PlatformConfig = { + projectConfig: (string, ProjectParams) => ?ProjectConfig, + dependencyConfig: (string, ProjectParams) => ?DependencyConfig, + linkConfig: () => { + isInstalled: (ProjectConfig, string, DependencyConfig) => boolean, + register: (string, DependencyConfig, Object, ProjectConfig) => void, + unregister: ( + string, + DependencyConfig, + ProjectConfig, + Array, + ) => void, + copyAssets: (string[], ProjectConfig) => void, + unlinkAssets: (string[], ProjectConfig) => void, }, - commands: string[], - platforms: PlatformsT, }; -type _ProjectConfigT = { +/** + * Final configuration object + */ +export type ConfigT = {| + // Root where the configuration has been resolved from root: string, + + // Path to React Native source + reactNativePath: string, + + // Object that contains configuration for a project (null, when platform not available) + project: { + android?: ?ProjectConfigAndroidT, + ios?: ?ProjectConfigIOST, + [key: string]: ?Object, + }, + + // An array of assets as defined by the user + assets: string[], + + // Map of the dependencies that are present in the project dependencies: { [key: string]: { + name: string, platforms: { - android: DependencyConfigAndroidT | null, - ios: DependencyConfigIOST | null, + android?: DependencyConfigAndroidT | null, + ios?: DependencyConfigIOST | null, [key: string]: any, }, assets: string[], - hooks: HooksT, + hooks: { + [key: string]: string, + prelink?: string, + postlink?: string, + }, params: InquirerPromptT[], }, }, - platforms: PlatformsT, + + // Map of available platforms (built-ins and dynamically loaded) + platforms: { + [name: string]: PlatformConfig, + ios?: PlatformConfig< + ProjectParamsIOST, + ProjectConfigIOST, + DependencyConfigIOST, + >, + android?: PlatformConfig< + ProjectParamsAndroidT, + ProjectConfigAndroidT, + DependencyConfigAndroidT, + >, + }, + + // An array of commands that are present in 3rd party packages commands: string[], + + // Haste configuration resolved based on available plugins haste: { platforms: Array, providesModuleNodeModules: Array, }, -}; +|}; + +/** + * Aliases + */ +export type DependencyConfigT = $PropertyType< + $PropertyType, + '[key: string]', +>; +export type HooksT = $PropertyType; +export type ProjectConfigT = $PropertyType; +export type PlatformsT = $PropertyType; + +/** + * Config defined by a developer for a library + */ +export type UserDependencyConfigT = { + // Additional dependency settings + dependency: { + platforms: { + android: DependencyParamsAndroidT, + ios: ProjectParamsIOST, + [key: string]: any, + }, + assets: string[], + hooks: HooksT, + params: InquirerPromptT[], + }, -export type RawProjectConfigT = _ProjectConfigT & { - reactNativePath: string | null, + // An array of commands that ship with the dependency + commands: string[], + + // An array of extra platforms to load + platforms: { + [name: string]: any, + }, }; -export type ProjectConfigT = _ProjectConfigT & { - reactNativePath: string, +/** + * Config defined by a developer for the project + */ +export type UserConfigT = { + /** + * Shares some structure with ConfigT, except that haste, root, platforms + * are calculated and can't be defined + */ + ...$Diff, + reactNativePath: ?string, + + // Additional project settings + project: { + android?: ProjectParamsAndroidT, + ios?: ProjectParamsIOST, + [key: string]: any, + }, }; + +// The following types are used in untyped-parts of the codebase, so I am leaving them +// until we actually need them. +type ProjectConfigIOST = {}; +type DependencyConfigIOST = ProjectConfigIOST; +type ProjectConfigAndroidT = {}; +type DependencyConfigAndroidT = {}; diff --git a/packages/cli/src/tools/errors.js b/packages/cli/src/tools/errors.js index 82bd9090c..6c2dfb159 100644 --- a/packages/cli/src/tools/errors.js +++ b/packages/cli/src/tools/errors.js @@ -4,6 +4,27 @@ import chalk from 'chalk'; import dedent from 'dedent'; +/** + * CLIError + * + * Features: + * - uses original stack trace when error object is passed + * - makes an inline string to match current styling inside CLI + */ +export class CLIError extends Error { + constructor(msg: string, error?: Error) { + super(msg.replace(/(\r\n|\n|\r)/gm, ' ')); + if (error) { + this.stack = error.stack + .split('\n') + .slice(0, 2) + .join('\n'); + } else { + Error.captureStackTrace(this, CLIError); + } + } +} + export class ProcessError extends Error { constructor(msg: string, processError: string) { super(`${chalk.red(msg)}\n\n${chalk.gray(processError)}`); @@ -11,39 +32,48 @@ export class ProcessError extends Error { } } +type JoiErrorDetails = { + message: string, + path: string[], + type: K, + context: T, +}; + type JoiErrorT = { - details: Array<{ - message: string, - path: string[], - type: string, - context: { - key: string, - label: string, - value: Object, - }, - }>, + details: Array< + JoiErrorDetails< + 'object.allowUnknown' | 'object.base' | 'string.base', + { + key: string, + label: string, + value: Object, + }, + >, + >, }; -export class JoiError extends Error { +export class JoiError extends CLIError { constructor(joiError: JoiErrorT) { super( joiError.details .map(error => { const name = error.path.join('.'); - const value = JSON.stringify(error.context.value); switch (error.type) { - case 'object.allowUnknown': + case 'object.allowUnknown': { + const value = JSON.stringify(error.context.value); return dedent` Unknown option ${name} with value "${value}" was found. This is either a typing error or a user mistake. Fixing it will remove this message. `; + } case 'object.base': - case 'string.base': + case 'string.base': { const expectedType = error.type.replace('.base', ''); const actualType = typeof error.context.value; return dedent` Option ${name} must be a ${expectedType}, instead got ${actualType} `; + } default: return error.message; } diff --git a/packages/cli/src/tools/findPlugins.js b/packages/cli/src/tools/findPlugins.js deleted file mode 100644 index 3ae92775c..000000000 --- a/packages/cli/src/tools/findPlugins.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import path from 'path'; -import {union, uniq, flatten} from 'lodash'; - -const RNPM_PLUGIN_PATTERNS = [/^rnpm-plugin-/, /^@(.*)\/rnpm-plugin-/]; - -const REACT_NATIVE_PLUGIN_PATTERNS = [ - /^react-native-/, - /^@(.*)\/react-native-/, - /^@react-native(.*)\/(?!rnpm-plugin-)/, -]; - -/** - * Filter dependencies by name pattern - * @param {String} dependency Name of the dependency - * @return {Boolean} If dependency is a rnpm plugin - */ -const isRNPMPlugin = dependency => - RNPM_PLUGIN_PATTERNS.some(pattern => pattern.test(dependency)); -const isReactNativePlugin = dependency => - REACT_NATIVE_PLUGIN_PATTERNS.some(pattern => pattern.test(dependency)); - -const readPackage = folder => { - try { - 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 findPlatformsInPackage = pjson => { - if (!pjson.rnpm || !pjson.rnpm.platform) { - return []; - } - - return path.join(pjson.name, pjson.rnpm.platform); -}; - -const getEmptyPluginConfig = () => ({ - commands: [], - platforms: [], - haste: { - platforms: [], - providesModuleNodeModules: [], - }, -}); - -const findHasteConfigInPackageAndConcat = (pjson, haste) => { - if (!pjson.rnpm || !pjson.rnpm.haste) { - return; - } - const pkgHaste = pjson.rnpm.haste; - - if (pkgHaste.platforms) { - haste.platforms = haste.platforms.concat(pkgHaste.platforms); - } - - if (pkgHaste.providesModuleNodeModules) { - haste.providesModuleNodeModules = haste.providesModuleNodeModules.concat( - pkgHaste.providesModuleNodeModules, - ); - } -}; - -const findPluginsInFolder = folder => { - const pjson = readPackage(folder); - - if (!pjson) { - return getEmptyPluginConfig(); - } - - const deps = union( - Object.keys(pjson.dependencies || {}), - Object.keys(pjson.devDependencies || {}), - ); - - return deps.reduce((acc, pkg) => { - let {commands, platforms} = acc; - if (isRNPMPlugin(pkg)) { - commands = commands.concat(pkg); - } - if (isReactNativePlugin(pkg)) { - const pkgJson = readPackage(path.join(folder, 'node_modules', pkg)); - if (pkgJson) { - commands = commands.concat(findPluginsInReactNativePackage(pkgJson)); - platforms = platforms.concat(findPlatformsInPackage(pkgJson)); - findHasteConfigInPackageAndConcat(pkgJson, acc.haste); - } - } - return {commands, platforms, haste: acc.haste}; - }, getEmptyPluginConfig()); -}; - -/** - * Find plugins in package.json of the given folder - * @param {String} folder Path to the folder to get the package.json from - */ -export default function findPlugins(folder: string) { - const plugin = findPluginsInFolder(folder); - return { - commands: uniq(flatten(plugin.commands)), - platforms: uniq(flatten(plugin.platforms)), - haste: { - platforms: uniq(flatten(plugin.haste.platforms)), - providesModuleNodeModules: uniq( - flatten(plugin.haste.providesModuleNodeModules), - ), - }, - }; -} diff --git a/packages/cli/src/tools/getPackageConfiguration.js b/packages/cli/src/tools/getPackageConfiguration.js deleted file mode 100644 index b1ce140e5..000000000 --- a/packages/cli/src/tools/getPackageConfiguration.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @flow - */ -import path from 'path'; -import type {PackageConfigurationT} from './types.flow'; - -/** - * Returns configuration of the CLI from `package.json`. - */ -export default function getPackageConfiguration( - folder: string, -): PackageConfigurationT { - return require(path.join(folder, './package.json')).rnpm || {}; -} diff --git a/packages/cli/src/tools/getParams.js b/packages/cli/src/tools/getParams.js deleted file mode 100644 index d8a528b60..000000000 --- a/packages/cli/src/tools/getParams.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @flow - */ - -import getPackageConfiguration from './getPackageConfiguration'; - -export default function getParams(root: string) { - const config = getPackageConfiguration(root); - return config.params || []; -} diff --git a/packages/cli/src/tools/getPlatforms.js b/packages/cli/src/tools/getPlatforms.js deleted file mode 100644 index 2aefde45d..000000000 --- a/packages/cli/src/tools/getPlatforms.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @flow - */ - -import path from 'path'; -import type {PlatformsT} from './types.flow'; - -import findPlugins from './findPlugins'; - -/** - * Support for `ios` and `android` platforms is built-in - * - * @todo(grabbou): Move this out of the core to increase "interoperability" - * with other platforms - */ -const builtInPlatforms = { - ios: require('./ios'), - android: require('./android'), -}; - -/** - * Returns an object with available platforms - */ -export default function getPlatforms(root: string): PlatformsT { - const plugins = findPlugins(root); - - /** - * Each `platfom` is a file that should define an object with platforms available - * and the config required. - * - * @todo(grabbou): We should validate if the config loaded is correct, warn and skip - * using it if it's invalid. - */ - const projectPlatforms = plugins.platforms.reduce( - (acc, pathToPlatform) => - Object.assign( - acc, - require(path.join(root, 'node_modules', pathToPlatform)), - ), - {}, - ); - - return { - ...builtInPlatforms, - ...projectPlatforms, - }; -} - -const names = { - ios: 'iOS', - android: 'Android', -}; - -export function getPlatformName(name: string) { - return names[name] || name; -} diff --git a/packages/cli/src/tools/loadMetroConfig.js b/packages/cli/src/tools/loadMetroConfig.js index 608b73c48..8a380f0d9 100644 --- a/packages/cli/src/tools/loadMetroConfig.js +++ b/packages/cli/src/tools/loadMetroConfig.js @@ -35,11 +35,8 @@ export const getDefaultConfig = (ctx: ContextT) => { resolver: { resolverMainFields: ['react-native', 'browser', 'main'], blacklistRE: getBlacklistRE(), - platforms: ['ios', 'android', 'native', ...ctx.haste.platforms], - providesModuleNodeModules: [ - 'react-native', - ...ctx.haste.providesModuleNodeModules, - ], + platforms: [...ctx.haste.platforms, 'native'], + providesModuleNodeModules: ctx.haste.providesModuleNodeModules, hasteImplModulePath: path.join(ctx.reactNativePath, 'jest/hasteImpl'), }, serializer: { diff --git a/packages/cli/src/tools/types.flow.js b/packages/cli/src/tools/types.flow.js index 32682a3e2..5926e655c 100644 --- a/packages/cli/src/tools/types.flow.js +++ b/packages/cli/src/tools/types.flow.js @@ -2,7 +2,7 @@ * @flow */ -import {type ProjectConfigT as ConfigT} from './config/types.flow'; +import {type ConfigT} from './config/types.flow'; export type ContextT = ConfigT; @@ -40,117 +40,3 @@ export type ProjectCommandT = LocalCommandT & { * Main type. Can be either local or a project command. */ export type CommandT = LocalCommandT | ProjectCommandT; - -/** - * Config of a single platform - */ -export type PlatformConfigT = { - projectConfig: (string, ParamsT) => ?ProjectConfigT, - dependencyConfig: (string, ParamsT) => ?DependencyConfigT, - /** - * @todo(grabbou): This should not be part of the "core". It should be - * specific to `link` and `unlink`. Remove it from here soon. - */ - linkConfig: () => { - /** - * @todo(grabbou): Revert the arguments order to align with the rest - */ - isInstalled: (ProjectConfigT, string, DependencyConfigT) => boolean, - register: (string, DependencyConfigT, Object, ProjectConfigT) => void, - unregister: ( - string, - DependencyConfigT, - ProjectConfigT, - Array, - ) => void, - copyAssets: (string[], ProjectConfigT) => void, - unlinkAssets: (string[], ProjectConfigT) => void, - }, -}; - -export type AndroidConfigParamsT = { - sourceDir?: string, - manifestPath?: string, - packageImportPath?: string, - packageInstance?: string, -}; - -export type IOSConfigParamsT = { - project?: string, - sharedLibraries?: string[], - libraryFolder?: string, -}; - -export type ProjectConfigIOST = {}; - -export type DependencyConfigIOST = ProjectConfigIOST; - -export type ProjectConfigAndroidT = {}; - -export type DependencyConfigAndroidT = {}; - -/** - * Config of a project. - * - * When one of the projects is `null`, that means given platform - * is not available in the current project. - */ -export type ProjectConfigT = { - android: ?ProjectConfigAndroidT, - ios: ?ProjectConfigIOST, -}; - -/** - * Config of a dependency. Just like above, when one of the values is `null`, - * given platform is not available. - */ -export type DependencyConfigT = { - android: ?DependencyConfigAndroidT, - ios: ?DependencyConfigIOST, -}; - -export type DependenciesConfig = { - config: DependencyConfigT, - name: string, - path: string, - assets: string[], - commands: {[name: string]: string}, - params: InquirerPromptT[], -}; - -/** - * Available platforms. Additional plugins should assert the type on their own. - */ -export type PlatformsT = { - ios: PlatformConfigT< - ProjectConfigIOST, - DependencyConfigIOST, - IOSConfigParamsT, - >, - android: PlatformConfigT< - ProjectConfigAndroidT, - DependencyConfigAndroidT, - AndroidConfigParamsT, - >, - [name: string]: PlatformConfigT, -}; - -export type InquirerPromptT = any; - -/** - * Configuration of the CLI as set by a package in the package.json - */ -export type PackageConfigurationT = { - assets?: string[], - commands?: {[name: string]: string}, - params?: InquirerPromptT[], - android: AndroidConfigParamsT, - ios: IOSConfigParamsT, - - plugin?: string | Array, - platform?: string, - haste?: { - platforms?: Array, - providesModuleNodeModules?: Array, - }, -};