Skip to content

Commit

Permalink
Showing 6 changed files with 195 additions and 96 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- rename default extension to core extension
- when a nested dependency is imported directly, re-link all its dependents
- [#527](https://github.com/teambit/bit/issues/527) rename structure property in bit.json
- support yarn as package manager
- add package manager config to bit.json

## [0.12.0-ext.11] - 2018-01-02

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -38,13 +38,13 @@
"bit-javascript": "0.10.8-dev.4",
"buffer-from": "^0.1.1",
"chalk": "^2.1.0",
"child-process-promise": "^2.2.1",
"chokidar": "^1.7.0",
"cli-spinners": "^1.0.0",
"commander": "^2.12.2",
"comment-json": "^1.1.3",
"decamelize": "^1.2.0",
"doctrine": "^2.0.2",
"execa": "^0.8.0",
"fs-extra": "^4.0.1",
"fstream": "^1.0.10",
"glob": "^7.1.1",
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -61,6 +61,8 @@ export const DEFAULT_COMPILER_ID = NO_PLUGIN_TYPE;

export const DEFAULT_TESTER_ID = NO_PLUGIN_TYPE;

export const DEFAULT_PACKAGE_MANAGER = 'npm';

export const DEFAULT_EXTENSIONS = {
'ext-docs-parser': {
config: {},
59 changes: 54 additions & 5 deletions src/consumer/bit-json/consumer-bit-json.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,12 @@ import R from 'ramda';
import path from 'path';
import AbstractBitJson from './abstract-bit-json';
import { BitJsonNotFound, BitJsonAlreadyExists, InvalidBitJson } from './exceptions';
import { BIT_JSON, DEFAULT_COMPONENTES_DIR_PATH, DEFAULT_DEPENDENCIES_DIR_PATH } from '../../constants';
import {
BIT_JSON,
DEFAULT_COMPONENTES_DIR_PATH,
DEFAULT_DEPENDENCIES_DIR_PATH,
DEFAULT_PACKAGE_MANAGER
} from '../../constants';

function composePath(bitPath: string) {
return path.join(bitPath, BIT_JSON);
@@ -14,6 +19,26 @@ function hasExisting(bitPath: string): boolean {
return fs.existsSync(composePath(bitPath));
}

type consumerBitJsonProps = {
impl?: string,
spec?: string,
compiler?: string,
tester?: string,
dependencies?: { [string]: string },
saveDependenciesAsComponents?: boolean,
lang?: string,
distTarget?: ?string,
distEntry?: ?string,
componentsDefaultDirectory?: string,
dependenciesDirectory?: string,
bindingPrefix?: string,
extensions?: Object,
packageManager?: 'npm' | 'yarn',
packageManagerArgs?: string[],
packageManagerProcessOptions?: Object,
useWorkspaces?: boolean
};

export default class ConsumerBitJson extends AbstractBitJson {
distTarget: ?string; // path where to store build artifacts
// path to remove while storing build artifacts. If, for example the code is in 'src' directory, and the component
@@ -22,6 +47,10 @@ export default class ConsumerBitJson extends AbstractBitJson {
componentsDefaultDirectory: string;
dependenciesDirectory: string;
saveDependenciesAsComponents: boolean; // save hub dependencies as bit components rather than npm packages
packageManager: 'npm' | 'yarn'; // package manager client to use
packageManagerArgs: ?(string[]); // package manager client to use
packageManagerProcessOptions: ?Object; // package manager process options
useWorkspaces: boolean; // Enables integration with Yarn Workspaces

constructor({
impl,
@@ -36,22 +65,34 @@ export default class ConsumerBitJson extends AbstractBitJson {
componentsDefaultDirectory,
dependenciesDirectory,
bindingPrefix,
extensions
}) {
extensions,
packageManager = DEFAULT_PACKAGE_MANAGER,
packageManagerArgs,
packageManagerProcessOptions,
useWorkspaces = false
}: consumerBitJsonProps) {
super({ impl, spec, compiler, tester, dependencies, lang, bindingPrefix, extensions });
this.distTarget = distTarget;
this.distEntry = distEntry;
this.componentsDefaultDirectory = componentsDefaultDirectory || DEFAULT_COMPONENTES_DIR_PATH;
this.dependenciesDirectory = dependenciesDirectory || DEFAULT_DEPENDENCIES_DIR_PATH;
this.saveDependenciesAsComponents = saveDependenciesAsComponents || false;
this.packageManager = packageManager;
this.packageManagerArgs = packageManagerArgs;
this.packageManagerProcessOptions = packageManagerProcessOptions;
this.useWorkspaces = useWorkspaces;
}

toPlainObject() {
const superObject = super.toPlainObject();
const consumerObject = R.merge(superObject, {
componentsDefaultDirectory: this.componentsDefaultDirectory,
dependenciesDirectory: this.dependenciesDirectory,
saveDependenciesAsComponents: this.saveDependenciesAsComponents
saveDependenciesAsComponents: this.saveDependenciesAsComponents,
packageManager: this.packageManager,
packageManagerArgs: this.packageManagerArgs,
packageManagerProcessOptions: this.packageManagerProcessOptions,
useWorkspaces: this.useWorkspaces
});
if (this.distEntry || this.distTarget) {
const dist = {};
@@ -100,7 +141,11 @@ export default class ConsumerBitJson extends AbstractBitJson {
dist,
bindingPrefix,
extensions,
saveDependenciesAsComponents
saveDependenciesAsComponents,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
useWorkspaces
} = object;

return new ConsumerBitJson({
@@ -115,6 +160,10 @@ export default class ConsumerBitJson extends AbstractBitJson {
saveDependenciesAsComponents,
componentsDefaultDirectory,
dependenciesDirectory,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
useWorkspaces,
distTarget: R.propOr(undefined, 'target', dist),
distEntry: R.propOr(undefined, 'entry', dist)
});
35 changes: 26 additions & 9 deletions src/consumer/consumer.js
Original file line number Diff line number Diff line change
@@ -1105,6 +1105,11 @@ export default class Consumer {
componentsWithDependencies: ComponentWithDependencies[],
verbose: boolean = false
): Promise<*> {
const packageManager = this.bitJson.packageManager;
const packageManagerArgs = this.bitJson.packageManagerArgs;
const packageManagerProcessOptions = this.bitJson.packageManagerProcessOptions;
const useWorkspaces = this.bitJson.useWorkspaces;

// if dependencies are installed as bit-components, go to each one of the dependencies and install npm packages
// otherwise, if the dependencies are installed as npm packages, npm already takes care of that
const componentsWithDependenciesFlatten = R.flatten(
@@ -1114,17 +1119,29 @@ export default class Consumer {
: [oneComponentWithDependencies.component];
})
);

const componentDirs = componentsWithDependenciesFlatten.map(component => component.writtenPath);
loader.start(BEFORE_INSTALL_NPM_DEPENDENCIES);
const results = await Promise.all(
componentsWithDependenciesFlatten.map((component) => {
// don't pass the packages to npmClient.install function.
// otherwise, it'll try to npm install the packages in one line 'npm install packageA packageB' and when
// there are mix of public and private packages it fails with 404 error.
// passing an empty array, results in installing packages from the package.json file
return npmClient.install([], { cwd: component.writtenPath }, verbose);
})
);

// don't pass the packages to npmClient.install function.
// otherwise, it'll try to npm install the packages in one line 'npm install packageA packageB' and when
// there are mix of public and private packages it fails with 404 error.
// passing an empty array, results in installing packages from the package.json file
let results = await npmClient.install({
modules: [],
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
useWorkspaces,
dirs: componentDirs,
rootDir: this.getPath(),
verbose
});

loader.stop();
if (!Array.isArray(results)) {
results = [results];
}
results.forEach((result) => {
if (result) npmClient.printResults(result);
});
191 changes: 110 additions & 81 deletions src/npm-client/npm-client.js
Original file line number Diff line number Diff line change
@@ -1,120 +1,149 @@
// @flow
import childProcessP from 'child-process-promise';
import R, { mapObjIndexed, isNil, pipe, values, merge, toPairs, map, join, is } from 'ramda';
import decamelize from 'decamelize';
import execa from 'execa';
import R, { isNil, merge, toPairs, map, join, is } from 'ramda';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import logger from '../logger/logger';
import { DEFAULT_PACKAGE_MANAGER } from '../constants';

const spawn = childProcessP.spawn;
const objectToArray = obj => map(join('@'), toPairs(obj));
const rejectNils = R.reject(isNil);

type Options = {
cwd?: string,
global?: boolean,
save?: boolean,
saveDev?: boolean,
saveOptional?: boolean,
saveExact?: boolean,
saveBundle?: boolean,
force?: boolean,
dryRun?: boolean,
ignoreScripts?: boolean,
legacyBundling?: boolean,
noOptional?: boolean,
noShrinkwrap?: boolean
const defaultNpmArgs = [];
const defaultYarnArgs = [];
const defaultPackageManagerArgs = {
npm: defaultNpmArgs,
yarn: defaultYarnArgs
};

const defaults = {
cwd: process.cwd(),
global: false,
save: false,
verbose: false,
unsafePerm: true,
saveDev: false,
saveOptional: false,
saveExact: false,
saveBundle: false,
force: false,
dryRun: false,
ignoreScripts: false,
legacyBundling: false,
noOptional: false,
noShrinkwrap: false
};

const camelCaseToOptionCase = optName => '--' + decamelize(optName, '-'); // eslint-disable-line

const serializeOption = (bool, optName) => {
if (optName === 'cwd') return null;
if (!bool) return null;
return camelCaseToOptionCase(optName);
const defaultPackageManagerProcessOptions = {
cwd: process.cwd
};

const stripNonNpmErrors = (errors: string[]) => {
const stripNonNpmErrors = (errors: string) => {
// a workaround to remove all 'npm warn' and 'npm notice'.
// NPM itself returns them even when --loglevel = error or when --silent/--quiet flags are set
return errors
.join('')
.split('\n')
.filter(error => error.startsWith('npm ERR!'))
.filter(error => error.startsWith('npm ERR!') || error.startsWith('error'))
.join('\n');
};

/**
* when modules is empty, it runs 'npm install' without any package, which installs according to package.json file
* Pick only allowed to be overriden options
* @param {Object} userOptions
*/
const installAction = (
modules: string[] | string | { [string]: number | string },
userOpts: Options,
const getAllowdPackageManagerProcessOptions = (userOptions) => {
const allowdOptions = ['shell', 'env', 'extendEnv', 'uid', 'gid', 'preferLocal', 'localDir', 'timeout'];
return R.pick(allowdOptions, userOptions);
};

type installArgs = {
modules?: string[] | { [string]: number | string },
packageManager: 'npm' | 'yarn',
packageManagerArgs: string[],
packageManagerProcessOptions: Object,
useWorkspaces: boolean,
dirs: string[],
rootDir: ?string, // Used for yarn workspace
verbose: boolean
) => {
const options = merge(defaults, userOpts);
// Add npm verbose flag
if (verbose) {
options.verbose = true;
}
const flags = pipe(mapObjIndexed(serializeOption), rejectNils, values)(options);
};

/**
* Install packages in specific directory
*/
const _installInOneDirectory = ({
modules = [],
packageManager = DEFAULT_PACKAGE_MANAGER,
packageManagerArgs = [],
packageManagerProcessOptions = {},
dir,
verbose = false
}) => {
// Handle process options
const allowedPackageManagerProcessOptions = getAllowdPackageManagerProcessOptions(packageManagerProcessOptions);
const concretePackageManagerProcessOptions = merge(
defaultPackageManagerProcessOptions,
allowedPackageManagerProcessOptions
);
concretePackageManagerProcessOptions.cwd = dir || concretePackageManagerProcessOptions.cwd;
const cwd = concretePackageManagerProcessOptions.cwd;

// taking care of object case
modules = is(Object, modules) && !Array.isArray(modules) ? objectToArray(modules) : modules;
// taking care of string and no modules cases
// $FlowFixMe
modules = Array.isArray(modules) ? modules : (modules && [modules]) || []; // eslint-disable-line
const processedModules = is(Object, modules) && !Array.isArray(modules) ? objectToArray(modules) : modules;

const serializedModules = modules && modules.length > 0 ? ` ${modules.join(' ')}` : '';
const serializedFlags = flags && flags.length > 0 ? `${flags.join(' ')}` : '';
// Handle process args
const concretePackageManagerDefaultArgs = [
'install',
...processedModules,
...defaultPackageManagerArgs[packageManager]
];
const concretePackageManagerArgs = rejectNils(R.concat(concretePackageManagerDefaultArgs, packageManagerArgs));

fs.ensureDirSync(path.join(options.cwd, 'node_modules'));
logger.debug(`installing npm packages ${serializedModules} at ${options.cwd}`);
// Add npm verbose flag
if (verbose && packageManager === 'npm') {
concretePackageManagerArgs.push('--verbose');
}

const args = ['install', ...serializedModules.trim().split(' '), serializedFlags];
fs.ensureDirSync(path.join(cwd, 'node_modules'));
logger.debug(
`installing npm packages using ${packageManager} at ${cwd} with options:`,
concretePackageManagerProcessOptions,
`and args: ${concretePackageManagerArgs}`
);

// Set the shell to true to prevent problems with post install scripts when running as root
const promise = spawn('npm', args, { cwd: options.cwd, shell: true });
const childProcess = promise.childProcess;
const stdoutOutput = [];
const stderrOutput = [];
const packageManagerClientName = packageManager;
const childProcess = execa(
packageManagerClientName,
concretePackageManagerArgs,
concretePackageManagerProcessOptions
);

childProcess.stdout.on('data', data => stdoutOutput.push(data.toString()));
childProcess.stderr.on('data', data => stderrOutput.push(data.toString()));

let stdout;
return promise
.then(() => {
return childProcess
.then(({ stdout, stderr }) => {
stdout = verbose
? stdoutOutput.join('')
: `successfully ran npm install${serializedModules}${serializedFlags} at ${options.cwd}`;
const stderr = verbose ? stderrOutput.join('') : '';
? stdout
: `successfully ran ${packageManager} install at ${cwd} with args: ${concretePackageManagerArgs}`;
stderr = verbose ? stderr : '';
return { stdout, stderr };
})
.catch((err) => {
const stderr = verbose ? stderrOutput.join('') : stripNonNpmErrors(stderrOutput);
return Promise.reject(`${stderr}\n\n${err.message}`);
const stderr = verbose ? err.stderr : stripNonNpmErrors(err.stderr);
// return Promise.reject(`${stderr}\n\n${err.message}`);
return Promise.reject(`${stderr}`);
});
};

/**
* when modules is empty, it runs 'npm install' without any package, which installs according to package.json file
*/
const installAction = ({
modules,
packageManager = DEFAULT_PACKAGE_MANAGER,
packageManagerArgs = [],
packageManagerProcessOptions = {},
useWorkspaces = false,
dirs = [],
rootDir,
verbose = false
}: installArgs) => {
if (useWorkspaces) {
return _installInOneDirectory({
modules,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
dir: rootDir,
verbose
});
}
const promises = dirs.map(dir =>
_installInOneDirectory({ modules, packageManager, packageManagerArgs, packageManagerProcessOptions, dir, verbose })
);
return Promise.all(promises);
};

const printResults = ({ stdout, stderr }: { stdout: string, stderr: string }) => {
console.log(chalk.yellow(stdout)); // eslint-disable-line
console.log(chalk.yellow(stderr)); // eslint-disable-line

0 comments on commit afa53e7

Please sign in to comment.