diff --git a/.travis.yml b/.travis.yml index 80feee21d7..7b99b72d1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ addons: notifications: email: - XChen@walmartlabs.com +install: eval `fyn bash` && fyn --pg none install +script: eval `fyn bash` && npm test before_install: - - if [[ `npm -v` != 3* ]]; then npm i -g npm@3; npm --version; fi - npm install -g xclap-cli fyn diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56a18f50b6..d838364150 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,12 +6,12 @@ There are [few guidelines](#contributing-guidelines) that we request contributor ## Getting Started -This repo uses [Lerna] as a top level setup. +This repo uses [Lerna] as a top level setup and [fyn] to manage Node Modules. -- Install the `clap` command globally if you don't want to invoke from `node_modules/.bin`. +- Install these CLI tools globally: [xclap-cli] and [fyn] ```bash -$ npm install -g xclap-cli +$ npm install -g xclap-cli fyn ``` - Fork and clone the repo at @@ -56,7 +56,7 @@ We love PRs and appreciate any help you can offer. Please follow the guidelines #### Styling -We've now switched to use [prettier] to format all our code. +We've now switched to use [prettier] to format all our code. Our [prettier] settings are: `--print-width 100` @@ -127,3 +127,7 @@ Here is the documentation on a [gitbook] structure: { + try { + const path = Path.join(pkgPath, k); + const pkgFile = Path.join(path, "package.json"); + const pkgStr = Fs.readFileSync(pkgFile); + const pkgJson = JSON.parse(pkgStr); + acc[pkgJson.name] = Object.assign( + _.pick(pkgJson, [ + "name", + "version", + "dependencies", + "devDependencies", + "optionalDependencies", + "peerDependencies" + ]), + { + localDeps: [], + dependents: [], + indirectDeps: [], + path, + pkgFile, + pkgStr, + pkgJson, + installed: false + } + ); + } catch (e) {} + return acc; +}, {}); + +const circulars = []; + +function listDeps() { + const add = (name, deps) => { + const depPkg = packages[name]; + + _.each(deps, (semver, depName) => { + if (!packages.hasOwnProperty(depName)) return; + depPkg.localDeps.push(depName); + packages[depName].dependents.push(name); + }); + }; + + _.each(packages, (pkg, name) => { + add(name, pkg.dependencies); + add(name, pkg.devDependencies); + add(name, pkg.optionalDependencies); + }); +} + +function listIndirectDeps() { + let change = 0; + + const add = (info, deps) => { + _.each(deps, dep => { + const depPkg = packages[dep]; + if (info.localDeps.indexOf(dep) < 0 && info.indirectDeps.indexOf(dep) < 0) { + change++; + info.indirectDeps.push(dep); + depPkg.dependents.push(info.name); + } + if (depPkg.localDeps.indexOf(info.name) >= 0) { + const x = [info.name, depPkg.name].sort().join(","); + if (circulars.indexOf(x) < 0) { + circulars.push(x); + } + return; + } + add(info, depPkg.localDeps.concat(depPkg.indirectDeps)); + }); + }; + + _.each(packages, (pkg, name) => { + add(pkg, pkg.localDeps.concat(pkg.indirectDeps)); + }); + + if (change > 0) { + listIndirectDeps(); + } +} + +listDeps(); +listIndirectDeps(); + +const depMap = _.mapValues(packages, pkg => { + return _.pick(pkg, ["name", "localDeps", "indirectDeps", "dependents"]); +}); + +const uniqCirculars = _.uniq(circulars).map(x => x.split(",")); +const ignores = _.map(uniqCirculars, pair => { + const depA = packages[pair[0]].dependents.length; + const depB = packages[pair[1]].dependents.length; + return depA > depB ? pair[1] : pair[0]; +}); + +packages["electrode-webpack-reporter"].ignore = true; + +function install(pkg, queue) { + if (pkg.ignore) return true; + if (pkg.installed === "pending") return false; + if (pkg.installed) return true; + let pending = 0; + _.each(pkg.localDeps, depName => { + if (!install(packages[depName], queue)) pending++; + }); + + if (pending === 0 && !pkg.installed) { + queue.push(pkg); + pkg.installed = "pending"; + } + return false; +} + +const VisualExec = require("./visual-exec"); + +function updatePkgToLocal(pkg) { + if (pkg.ignore) return; + const json = pkg.pkgJson; + if (!json) return; + ["dependencies", "devDependencies", "optionalDependencies"].forEach(sec => { + const deps = json[sec]; + if (!deps) return; + _.each(pkg.localDeps, depName => { + if (!packages[depName].ignore && deps.hasOwnProperty(depName)) + deps[depName] = `../${depName}`; + }); + }); + Fs.writeFileSync(pkg.pkgFile, `${JSON.stringify(json, null, 2)}\n`); +} + +function restorePkgJson() { + _.each(packages, pkg => { + if (!pkg.ignore) Fs.writeFileSync(pkg.pkgFile, pkg.pkgStr); + }); +} + +_.each(packages, updatePkgToLocal); + +const itemQ = new ItemQueue({ + processItem: item => { + const command = [`eval "$(fyn bash)"`, `fyn -q i install`]; + if (_.get(item, "pkgJson.scripts.prepublish")) command.push("npm run prepublish"); + if (_.get(item, "pkgJson.scripts.prepare")) command.push("npm run prepare"); + return new VisualExec({ + title: `bootstrap ${item.name}`, + cwd: item.path, + command: command.join(" && ") + }).execute(); + }, + concurrency: 3, + stopOnError: false, + Promise: require("bluebird") +}); + +function getMoreInstall(item) { + if (item) item.installed = true; + + const queue = []; + + _.each(packages, (pkg, name) => { + install(pkg, queue); + }); + + queue.forEach(x => itemQ.addItem(x, true)); +} + +itemQ.on("doneItem", data => getMoreInstall(data.item)); + +let failed = 0; +itemQ.on("done", restorePkgJson); +itemQ.on("failItem", () => { + failed = 1; +}); +itemQ.on("fail", () => { + failed = 1; + restorePkgJson(); +}); + +getMoreInstall(); +itemQ.resume(); + +itemQ + .wait() + .then(() => { + process.exit(failed); + }) + .catch(err => { + console.log(err); + process.exit(1); + }); diff --git a/tools/logger.js b/tools/logger.js new file mode 100644 index 0000000000..c8e8f80293 --- /dev/null +++ b/tools/logger.js @@ -0,0 +1,9 @@ +"use strict"; + +const VisualLogger = require("visual-logger"); + +if (process.env.CI) { + console.log("CI env detected"); +} + +module.exports = new VisualLogger().setItemType(process.env.CI ? "simple" : "normal"); diff --git a/tools/visual-exec.js b/tools/visual-exec.js new file mode 100644 index 0000000000..d3d61df83d --- /dev/null +++ b/tools/visual-exec.js @@ -0,0 +1,142 @@ +"use strict"; + +/* eslint-disable no-magic-numbers,no-eval */ + +const Path = require("path"); +const assert = require("assert"); +const xsh = require("xsh"); +const Promise = require("bluebird"); +const chalk = require("chalk"); +const _ = require("lodash"); +const logger = require("./logger"); +const VisualLogger = require("visual-logger"); + +xsh.Promise = Promise; + +const ONE_MB = 1024 * 1024; + +function uniqId() { + return ( + Math.random() + .toString(36) + .substr(2, 10) + + "_" + + Date.now().toString(36) + ); +} + +class VisualExec { + constructor({ title = undefined, command, cwd = process.cwd() }) { + this._title = title || `Executing ${command}`; + this._command = command; + this._cwd = cwd; + } + + /* eslint-disable max-statements */ + _updateDigest(item, buf) { + if (item.buf.indexOf("\n") >= 0 || buf.indexOf("\n") >= 0) { + item.buf = buf; + } else { + item.buf += buf; + } + buf = item.buf; + buf = buf && buf.trim(); + if (buf) { + logger.updateItem( + item.name, + buf + .split("\n") + .map(x => x && x.trim()) + .join(chalk.blue("\\n")) + .substr(0, 100) + ); + } + } + + _logResult(child) { + const stdout = `stdout_${uniqId()}`; + const stderr = `stderr_${uniqId()}`; + + logger.addItem({ + name: stdout, + color: "green", + display: `=== ${this._title}\nstdout`, + spinner: VisualLogger.spinners[1] + }); + logger.addItem({ + name: stderr, + color: "red", + display: `stderr` + }); + + const stdoutDigest = { name: stdout, buf: "" }; + const stderrdigest = { name: stderr, buf: "" }; + const updateStdout = buf => this._updateDigest(stdoutDigest, buf); + const updateStderr = buf => this._updateDigest(stdoutDigest, buf); + + child.stdout.on("data", updateStdout); + child.stderr.on("data", updateStderr); + + const logResult = (err, output) => { + logger.removeItem(stdout); + logger.removeItem(stderr); + child.stdout.removeListener("data", updateStdout); + child.stderr.removeListener("data", updateStderr); + + const result = err ? `failed ${chalk.red(err.message)}` : chalk.green("exit code 0"); + + const info = () => (err ? "error" : "info"); + const verbose = () => (err ? "error" : "verbose"); + + if (err) { + output = err.output; + } + + logger[info()](`executed ${this._title} ${result}`); + + const colorize = t => t.replace(/ERR!/g, chalk.red("ERR!")); + + const logOutput = () => { + const logs = [chalk.green(">>>")]; + logs.push(`Start of output from ${this._title} ===`); + + if (output.stdout) logs.push(`\n${colorize(output.stdout)}`); + if (output.stderr) { + logs.push(chalk.red("\n=== stderr ===\n") + colorize(output.stderr)); + } + logs.push(chalk.blue("\n<<<")); + logs.push(`End of output from ${this._title} ---`); + logger.prefix(false)[verbose()].apply(logger, logs); + }; + + if (!output || (!output.stdout && !output.stderr)) { + logger[verbose()](`${chalk.green("No output")} from ${this._title}`); + } else { + logOutput(); + } + }; + + return child.promise.tap(output => logResult(null, output)).catch(err => { + logResult(err); + throw err; + }); + } + + execute() { + const scriptName = chalk.magenta(this._command); + + const child = xsh.exec( + { + silent: true, + cwd: this._cwd, + env: Object.assign({}, process.env, { PWD: this._cwd }), + maxBuffer: ONE_MB + }, + this._command + ); + + return this._logResult(child); + } +} + +module.exports = VisualExec;