diff --git a/.eslintrc.json b/.eslintrc.json index a37ed8f..3addd7d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,6 +26,9 @@ "eol-last": [ "error", "unix" + ], + "no-tabs": [ + "error" ] } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 470486d..e85147c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ node_modules/ playground/ yarn.lock /npm-debug.log + +\.vscode/ +package-lock.json diff --git a/bin/mondo.js b/bin/mondo.js index 489cf4d..363bf72 100755 --- a/bin/mondo.js +++ b/bin/mondo.js @@ -1,36 +1,32 @@ #!/usr/bin/env node -const fs = require('fs'); -const Path = require('path'); -const chalk = require('chalk'); +const File = require('phylo'); +const isDebug = /-{1,2}de?b?u?g?\b/.test(process.argv[2]); +const log = require('loog')({ + prefixStyle: 'ascii', + logLevel: isDebug ? 'debug' : 'info' +}); -let cwd = Path.resolve('.'); -let Mondo, mondoIndex; +let cwd = File.cwd().upToDir('node_modules'); +let Mondo, mondoCli; while (cwd) { - mondoIndex = Path.resolve(cwd, "node_modules/mondorepo/src/cli.js"); - if (fs.existsSync(mondoIndex)) { - Mondo = require(mondoIndex); + mondoCli = cwd.join("mondorepo/cli/index.js"); + if (mondoCli.exists()) { + log.debug(`Using local version from ${mondoCli.path}`); + Mondo = require(mondoCli.path); break; } else { - let parentDir = Path.resolve(cwd, '..'); - - if (parentDir === cwd) { - cwd = null; - } else { - cwd = parentDir; - } + cwd = cwd.parent && cwd.parent.parent ? cwd.parent.parent.parent.upToDir('node_modules') : null; } } if (!Mondo) { - Mondo = require('../src/cli.js'); + mondoCli = File.from(__dirname).parent.join("cli/index.js").absolutify(); + log.debug(`Using global version from ${mondoCli.path}`); + Mondo = require(mondoCli.path); } -const mondo = new Mondo(); -mondo.run().then(function (){}, - function (cause) { - console.log(""); - console.error(chalk.red(mondo.params.debug ? cause : cause.message)); - process.exit(1); - } -); +new Mondo(log).run().catch(e => { + log.error(isDebug ? e.stack : (e.message ? e.message : e)); + process.exit(1); +}); diff --git a/src/commands/Publish.js b/cli/Publish.js similarity index 61% rename from src/commands/Publish.js rename to cli/Publish.js index 4cb24d8..217c840 100644 --- a/src/commands/Publish.js +++ b/cli/Publish.js @@ -1,59 +1,62 @@ -"use strict"; -const Path = require('path'); -const fs = require('fs'); -const {Command} = require('switchit'); +const BaseCommand = require('./base/command'); + +const File = require('phylo'); const semver = require('semver'); const chalk = require('chalk'); const columnify = require('columnify'); -const jsonfile = require('jsonfile'); const JSON5 = require('json5'); -const NPM = require('../pkgMgrs/Npm.js'); -const Repo = require('../Repo.js'); -const Collection = require('../utils/Collection.js'); -const isWindows = /^win/.test(process.platform); -class Publish extends Command { - constructor() { - this.publisher = new NPM(); - } +const Npm = require('../src/npm.js'); +const Repo = require('../src/repo.js'); +const Collection = require('../src/collection.js'); +const isWindows = /^win/.test(process.platform); + +class Publish extends BaseCommand { execute(params) { - const {recursive, dry, script, 'check-existing': checkExisting} = params; - const path = params.path ? Path.isAbsolute(params.path) ? params.path : Path.join(process.cwd(), params.path) : process.cwd(); + let me = this; + + const {recursive, 'dry-run': dry, script, 'check-existing': checkExisting} = params; + const path = params.path ? File.from(params.path).isAbsolute() ? File.from(params.path) : File.cwd().join(params.path) : File.cwd(); const repo = Repo.open(path); - this._packages = new Collection(); + me.publisher = new Npm({ + log: me.log, + debug: me.root().params.debug + }); + + me._packages = new Collection(); - this.hasPublishConflict = false; - this.dry = script ? false : dry; - this.script = script; - this.checkExisting = checkExisting; + me.hasPublishConflict = false; + me.dry = script ? false : dry; + me.script = script; + me.checkExisting = checkExisting; // Get a list of all the packages we will be publishing for (let pkg of (recursive ? repo.allPackages : repo.packages)) { if (!pkg.private) { - this._packages.add(pkg); + me._packages.add(pkg); } } if (checkExisting) { - return this.doCheckExisting() + return me.doCheckExisting() .then(() => { - if (this.dry || this.hasPublishConflict) { - this.log(); + if (me.dry || me.hasPublishConflict) { + me.writeSummary(); } else if (script) { - this.writeScript(); + me.writeScript(); } else { - this.log(); - return this.publish(); + me.writeSummary(); + return me.publish(); } }); } else { if (script) { - this.writeScript(); + me.writeScript(); } else { - this.log(); - return this.publish(); + me.writeSummary(); + return me.publish(); } } } @@ -91,7 +94,8 @@ class Publish extends Command { })); } - log() { + writeSummary () { + let me = this; let columns = Array.from(this._packages.items); let statusRegExp = /^ (W|E) /g; let statusRegExpResult, colorFunc; @@ -109,11 +113,28 @@ class Publish extends Command { } }); + let colwidth = ((process.stdout.columns - 3) / 3); columns = columnify(columns, { - showHeaders: false, - minWidth: 20, + showHeaders: true, + minWidth: colwidth, + maxLineWidth: 'auto', config: { - status: {align: 'center', minWidth: 3} + status: { + align: 'center', + headingTransform: () => 'S\n···', + minWidth: 3 + }, + name: { + headingTransform: () => chalk.bold('Name')+'\n····', + maxWidth: colwidth + }, + version: { + headingTransform: () => chalk.bold('Version')+'\n·······', + maxWidth: colwidth + }, + details: { + headingTransform: () => chalk.bold('Details')+'\n·······' + } }, columns: ['status', 'name', 'version', 'details'] }); @@ -127,9 +148,9 @@ class Publish extends Command { return colorFunc(row); } return row; - }).join('\n'); + }); - console.log(columns); + columns.forEach(l => me.log.log(l)); } publish() { @@ -137,14 +158,13 @@ class Publish extends Command { return this._packages.reduce((promise, pkg) => { return promise.then(() => { const json = pkg.publishify(); - const original = fs.readFileSync(pkg.file); - jsonfile.writeFileSync(pkg.file, json, {spaces: 4}); - + const original = pkg.file.load(); + jsonfile.writeFileSync(pkg.file.path, json, {spaces: 4}); return this.publisher.publish(pkg.path).then(r => { - fs.writeFileSync(pkg.file, original); + pkg.file.save(original); return r; }).catch(err => { - fs.writeFileSync(pkg.file, original); + pkg.file.save(original); if (this.checkExisting || !err.message.includes('You cannot publish over the previously published version')) { throw err; } else { @@ -164,13 +184,14 @@ class Publish extends Command { } writeScript() { + let me = this; const prefix = isWindows ? 'REM' : '#'; this._packages.forEach(pkg => { if (!pkg.$$alreadyPublished) { - console.log(`npm publish ${pkg.path}`); + me.log.log(`npm publish ${pkg.path}`); } else { - console.log(`${prefix} Version already exists for ${pkg.name}`); - console.log(`${prefix} npm publish ${pkg.path}`); + me.log.log(`${prefix} Version already exists for ${pkg.name}`); + me.log.log(`${prefix} npm publish ${pkg.path}`); } }); } @@ -178,13 +199,14 @@ class Publish extends Command { Publish.define({ help: { - '': 'Rev version of packages from the current repo' + '': 'Rev version of packages from the current repo', + 'dry-run': 'Show a summary of changes to perform, leaves everything intact', + 'script': 'Outputs a script to perform the operations manually', + 'check-existing': 'Compare against published versions in the npm registry', + 'recursive': 'Process all known packages (including those inside used repositories)' }, parameters: '[path=]', - switches: `[dry:boolean=false] - [script:boolean=false] - [check-existing:boolean=true] - [recursive:boolean=false]` + switches: '[dry-run:boolean=false] [script:boolean=false] [check-existing:boolean=true] [recursive:boolean=false]' }); diff --git a/src/commands/Rev.js b/cli/Rev.js similarity index 69% rename from src/commands/Rev.js rename to cli/Rev.js index 5139f16..6538dba 100644 --- a/src/commands/Rev.js +++ b/cli/Rev.js @@ -1,15 +1,15 @@ -"use strict"; +const BaseCommand = require('./base/command'); -const Path = require('path'); -const {Command} = require('switchit'); +const File = require('phylo'); const semver = require('semver'); const chalk = require('chalk'); const columnify = require('columnify'); const JSON5 = require('json5'); const jsonfile = require('jsonfile'); -const NPM = require('../pkgMgrs/Npm.js'); -const Repo = require('../Repo.js'); -const Collection = require('../utils/Collection.js'); + +const Npm = require('../src/npm.js'); +const Repo = require('../src/repo.js'); +const Collection = require('../src/collection.js'); class RevPackage { constructor(pkg) { @@ -76,26 +76,33 @@ class RevPackage { } -class Rev extends Command { +class Rev extends BaseCommand { execute(params) { - const path = params.path ? Path.isAbsolute(params.path) ? params.path : Path.join(process.cwd(), params.path) : process.cwd(); + let me = this; + + const path = params.path ? File.from(params.path).isAbsolute() ? File.from(params.path) : File.cwd().join(params.path) : File.cwd(); const version = params.version.raw !== '0.0.0' ? params.version : false; - const {preid, increment, recursive, dry, modified: checkModified, 'check-existing': checkExisting} = params; + const {preid, increment, recursive, 'dry-run': dry, 'check-modified': checkModified, 'check-existing': checkExisting} = params; const repo = Repo.open(path); - this._revPackages = new Collection(); - this.checkModified = checkModified; - this.checkExisting = checkExisting; - this.dry = dry; + me._revPackages = new Collection(); + me.checkModified = checkModified; + me.checkExisting = checkExisting; + me.dry = dry; + + me.npm = new Npm({ + log: me.log, + debug: me.root().params.debug + }); // Get a list of all the this._revPackages we will be reving for (let pkg of (recursive ? repo.allPackages : repo.packages)) { - this._revPackages.add(new RevPackage(pkg)); + me._revPackages.add(new RevPackage(pkg)); } // Increment or set the version for this package in memory - for (let revPkg of this._revPackages) { + for (let revPkg of me._revPackages) { if (version) { if (semver.neq(revPkg.version, version)) { revPkg.version = version; @@ -107,22 +114,22 @@ class Rev extends Command { } } - return this.updateRegistryData() - .then(this.updatePublishableDependencies.bind(this)) - .then(this.logRev.bind(this)) - .then(this.writeRev.bind(this)); + return me.updateRegistryData() + .then(me.updatePublishableDependencies.bind(me)) + .then(me.logRev.bind(me)) + .then(me.writeRev.bind(me)); } updateRegistryData() { - const checkExisting = this.checkExisting; - const checkModified = this.checkModified; + let me = this; + const checkExisting = me.checkExisting; + const checkModified = me.checkModified; if (checkExisting || checkModified) { - const npm = new NPM(); return Promise.all( - this._revPackages.map(revPkg => { + me._revPackages.map(revPkg => { // Run NPM view over the package to get registry data - return npm.view(revPkg.name, revPkg.originalVersion) + return me.npm.view(revPkg.name, revPkg.originalVersion) .then(results => { const registry = revPkg.registry = !!results ? JSON5.parse(results) : false; @@ -174,6 +181,7 @@ class Rev extends Command { } logRev() { + let me = this; const log = []; let statusRegExp = /^ (W|E) /g; @@ -211,11 +219,28 @@ class Rev extends Command { log.push(pkgLog); }); + let colwidth = ((process.stdout.columns - 3) / 3); columns = columnify(log, { - showHeaders: false, - minWidth: 20, + showHeaders: true, + minWidth: colwidth, + maxLineWidth: 'auto', config: { - status: {align: 'center', minWidth: 3} + status: { + align: 'center', + headingTransform: () => 'S\n···', + minWidth: 3 + }, + name: { + headingTransform: () => chalk.bold('Name')+'\n····', + maxWidth: colwidth + }, + version: { + headingTransform: () => chalk.bold('Version')+'\n·······', + maxWidth: colwidth + }, + details: { + headingTransform: () => chalk.bold('Details')+'\n·······' + } }, columns: ['status', 'name', 'version', 'details'] }); @@ -229,9 +254,9 @@ class Rev extends Command { return colorFunc(row); } return row; - }).join('\n'); + }); - console.log(columns); + columns.forEach(l => me.log.log(l)); } writeRev() { @@ -242,7 +267,7 @@ class Rev extends Command { const pkg = revPkg.package; const manifest = pkg.package; manifest.version = revPkg.version.raw; - jsonfile.writeFileSync(Path.join(pkg.path, 'package.json'), manifest, {spaces: 4}); + jsonfile.writeFileSync(pkg.path.join('package.json').path, manifest, {spaces: 4}); } }); } @@ -252,18 +277,21 @@ class Rev extends Command { Rev.define({ help: { '': 'Rev version of packages from the current repo', - 'dry': 'Dry run, will not modify any files' + 'dry-run': 'Show a summary of changes to perform, leaves everything intact', + 'check-existing': 'Compare against published versions in the npm registry', + 'check-modified': 'Compare against the hash of the latest published version', + 'recursive': 'Process all known packages (including those inside used repositories)', + 'increment': 'The increment to the version to apply (major, minor, patch, or prerelease)', + 'preid': 'Used when incrementing for a prerelease (eg. The "alpha" in 1.0.0-alpha.1)' }, parameters: '[path=]', - switches: `[dry:boolean=false] - [check-existing:boolean=true] - [modified:boolean=false] - - [force-patch-version-sync:boolean=false] - [recursive:boolean=false] - [increment:string=patch] - [preid:string=] - [version:semver=0.0.0]` + switches: `[dry-run:boolean=false] + [check-existing:boolean=true] + [check-modified:boolean=false] + [recursive:boolean=false] + [increment:string=patch] + [preid:string=] + [version:semver=0.0.0]` }); module.exports = Rev; diff --git a/cli/base/command.js b/cli/base/command.js new file mode 100644 index 0000000..d7ad955 --- /dev/null +++ b/cli/base/command.js @@ -0,0 +1,15 @@ +const {Command} = require('switchit'); + +class BaseCommand extends Command { + attach (parent) { + super.attach(parent); + let root = this.root(); + this.log = root.log; + this.config = root.config; + this.rootDir = root.rootDir; + this.debug = root.debug; + return this; + } +} + +module.exports = BaseCommand; diff --git a/src/commands/Fork/Add.js b/cli/fork/add.js similarity index 54% rename from src/commands/Fork/Add.js rename to cli/fork/add.js index 15560e9..7c04561 100644 --- a/src/commands/Fork/Add.js +++ b/cli/fork/add.js @@ -1,35 +1,29 @@ -const Command = require('switchit').Command; +const BaseCommand = require('../base/command'); const chalk = require('chalk'); -const Logger = require('../../utils/Logger'); -const FileUtils = require('../../utils/FileUtil'); - -class Add extends Command { +class Add extends BaseCommand { execute (params) { let me = this; - let mondo = me.root(); - - let forks = mondo.settings.forks; + let forks = me.config.get('forks') || {}; let replace = false; if (forks[params.repoName]) { if (params.force) { replace = true; - Logger.warn(`Replaced ${chalk.bold.yellow(forks[params.repoName])} with ${chalk.bold.yellow(params.forkName)} as fork for ${chalk.bold.yellow(params.repoName)}.`) + me.log.warn(`Replacing ${chalk.bold.yellow(forks[params.repoName])} with ${chalk.bold.yellow(params.forkName)} as fork for ${chalk.bold.yellow(params.repoName)}.`); } else { - Logger.error(`${chalk.bold.yellow(forks[params.repoName])} is already configured as fork for ${chalk.bold.yellow(params.repoName)}.`); + me.log.error(`${chalk.bold.yellow(forks[params.repoName])} is already configured as fork for ${chalk.bold.yellow(params.repoName)}.`); if (forks[params.repoName] !== params.forkName) { - Logger.info(''); - Logger.info(`Use ${chalk.bold.yellow('--force')} to overwrite it.`); + me.log.error(`Use ${chalk.bold.yellow('--force')} to overwrite it.`); } return; } } forks[params.repoName] = params.forkName; - FileUtils.writeFile(mondo.settingsPath, JSON.stringify(mondo.settings, null, ' ')); + me.config.set('forks', forks); if (!replace) { - Logger.info(`Added ${chalk.bold.yellow(params.forkName)} as known fork for ${chalk.bold.yellow(params.repoName)}.`); + me.log.info(`Added ${chalk.bold.yellow(params.forkName)} as known fork for ${chalk.bold.yellow(params.repoName)}.`); } } } @@ -45,4 +39,4 @@ Add.define({ parameters: '{repoName} {forkName}' }); -module.exports = Add; \ No newline at end of file +module.exports = Add; diff --git a/cli/fork/index.js b/cli/fork/index.js new file mode 100644 index 0000000..baac99f --- /dev/null +++ b/cli/fork/index.js @@ -0,0 +1,19 @@ +const Container = require('switchit').Container; + +const Add = require('./add'); +const List = require('./list'); +const Remove = require('./remove'); + +class Fork extends Container {} + +Fork.define({ + help: 'Commands to manage the global set of known forks', + commands: { + '': 'list', + 'add': Add, + 'list': List, + 'remove': Remove + } +}); + +module.exports = Fork; diff --git a/src/commands/Fork/List.js b/cli/fork/list.js similarity index 73% rename from src/commands/Fork/List.js rename to cli/fork/list.js index 4937e18..9ebad32 100644 --- a/src/commands/Fork/List.js +++ b/cli/fork/list.js @@ -1,17 +1,15 @@ -const Command = require('switchit').Command; +const BaseCommand = require('../base/command'); const columnify = require('columnify'); const chalk = require('chalk'); -const Logger = require('../../utils/Logger'); - -class List extends Command { +class List extends BaseCommand { execute (params) { let me = this; - let mondo = me.root(); + let forks = me.config.get('forks'); - if (mondo.settings.forks && Object.keys(mondo.settings.forks).length) { - Logger.info(`The following forks are configured in your settings file.`); - Logger.info(''); + if (forks && Object.keys(forks).length) { + me.log.info(`The following forks are configured in your settings file.`); + me.log.log(''); /* * The following block outputs a table-like layout with widths based on the * number of columns in the tty write stream (process.stdout) @@ -19,9 +17,9 @@ class List extends Command { * By default `columnify` prints column headers in uppercase without divider * but I'm not a fan of that, hence the `headingTransform` functions below. */ - console.log( + me.log.log( columnify( - mondo.settings.forks, + forks, { columns: ['repo', 'fork'], minWidth: (process.stdout.columns / 3), @@ -43,10 +41,10 @@ class List extends Command { } ).split('\n').map((l) => ` ${l}`).join('\n') // This indents the lines produced by `columnify` ); - Logger.info(''); - Logger.info(`Use ${chalk.bold.yellow('mondo fork (add|remove)')} to manage them.`); + me.log.log(''); + me.log.info(`Use ${chalk.bold.yellow('mondo fork (add|remove)')} to manage them.`); } else { - Logger.info('There are no known forks in the global set'); + me.log.info('There are no known forks in the global set'); } } } @@ -55,4 +53,4 @@ List.define({ help: 'Displays the global set of known forks' }); -module.exports = List; \ No newline at end of file +module.exports = List; diff --git a/cli/fork/remove.js b/cli/fork/remove.js new file mode 100644 index 0000000..2d0a37c --- /dev/null +++ b/cli/fork/remove.js @@ -0,0 +1,29 @@ +const BaseCommand = require('../base/command'); +const chalk = require('chalk'); + +class Remove extends BaseCommand { + execute (params) { + let me = this; + let forks = me.config.get('forks'); + + if (!forks[params.repoName]) { + me.log.info(`There is no known fork for ${chalk.bold.yellow(params.repoName)}.`); + return; + } + + let old = forks[params.repoName]; + delete forks[params.repoName]; + me.config.set('forks', forks); + me.log.info(`Removed ${chalk.bold.yellow(old)} as known fork for ${chalk.bold.yellow(params.repoName)}.`); + } +} + +Remove.define({ + help: { + '': 'Removes the known fork for a repository', + repoName: 'The name of the repository' + }, + parameters: '{repoName}' +}); + +module.exports = Remove; diff --git a/cli/index.js b/cli/index.js new file mode 100644 index 0000000..13b38e7 --- /dev/null +++ b/cli/index.js @@ -0,0 +1,49 @@ +const File = require('phylo'); + +const {Container} = require('switchit'); +const Install = require('./install'); +const Fork = require('./fork'); +const Publish = require('./publish'); +const Rev = require('./rev'); + +const Config = require('../src/config'); + +class Mondo extends Container { + constructor (log) { + super(); + this.log = log || require('loog')({prefixStyle: 'ascii'}); + } + + configure (args) { + if (!this.config) { + this.config = new Config(); + this.config.set('pkg', this.rootDir.join('package.json').load()); + } + return super.configure(args); + } + + execute (params, args) { + let me = this; + if (params.debug) { + me.log.setLogLevel('debug'); + me.debug = true; + } + return super.execute(params, args); + } +} + +Mondo.define({ + help: { + '': 'Management for collections of packages across teams', + debug: 'Provide debug logging output' + }, + switches: '[debug:boolean=false]', + commands: { + 'fork': Fork, + 'install': Install, + 'publish': Publish, + 'rev': Rev + } +}); + +module.exports = Mondo; diff --git a/cli/install.js b/cli/install.js new file mode 100644 index 0000000..1b91b68 --- /dev/null +++ b/cli/install.js @@ -0,0 +1,126 @@ +const BaseCommand = require('./base/command'); + +const chalk = require('chalk'); + +const fs = require('fs'); +const File = require('phylo'); +const Repo = require('../src/repo'); +const VCS = require('../src/vcs'); +const Npm = require('../src/npm'); + +const {promiseSerial} = require('../src/Util'); +const isWindows = /^win/.test(process.platform); + +class install extends BaseCommand { + execute (params) { + let me = this; + Repo.log = me.log; + + me.log.debug('Initializing git bridge'); + me.vcs = VCS.git({ + forks: params.forks ? me.config.get('forks') || {} : {}, + config: me.config, + log: me.log, + debug: me.root().params.debug + }); + + if (!me.vcs.available()) { + throw new Error('Make sure `git` is available in your PATH'); + } + + me.log.debug('Initializing npm bridge'); + me.npm = new Npm({ + log: me.log, + debug: me.root().params.debug + }); + + return me.installRepo().then((repo) => { + me.log.debug('Linking all packages in the mondoverse'); + return promiseSerial(repo.allPackages.map(pkg => () => { + me.log.debug(`Processing ${pkg.repo.name}:${pkg.name}`); + let mondodeps = pkg.mondoDependencies.map(p => p.path.relativePath(pkg.path)); + mondodeps.forEach((p) => { + // TODO: Remove this once https://github.com/npm/npm/issues/17257 is dealt with + File.from(p).join('node_modules').remove('r'); + File.from(p).join('package-lock.json').remove(); + }); + pkg.path.join('package-lock.json').remove(); + if (mondodeps.length > 0) { + return me.npm.install(pkg.path, { + save: false, + pkg: mondodeps.join(' ') + }); + } else { + return Promise.resolve(true); + } + })); + }); + } + + installPackages (repo) { + let me = this; + me.log.info(`Installing packages for '${chalk.magenta(repo.name)}'`).indent(); + return promiseSerial(repo.packages.map(pkg => () => { + me.log.info(`Installing '${chalk.magenta(repo.name)}:${chalk.yellow(pkg.name)}'`); + return me.npm.install(pkg.path); + })).then(() => { + me.log.outdent(); + return repo + }); + } + + installRepo (repo) { + let me = this; + let promise; + + if (!repo) { + repo = Repo.open(File.cwd(), me.config).root; + } + + if (repo.installed) { + return Promise.resolve(repo); + } + + repo.installed = true; + if (repo.exists()) { + promise = Promise.resolve(repo); + } else { + promise = me.vcs.clone(repo.source.repository, repo.path, repo.source.branch); + } + return promise.then(() => me.installUsed(repo)).then(() => me.installPackages(repo)); + } + + installUsed (repo) { + let me = this; + me.writeManifestFile(repo); + repo.open(); + + if (repo.uses.length > 0) { + me.log.debug(`Installing repositories used by '${repo.name}'`); + return promiseSerial(repo.uses.map(child => () => me.installRepo(child))) + .then(() => repo); + } + return repo; + } + + writeManifestFile (repo) { + let me = this; + let manifest = File.from(repo.path).join(me.config.get('child')); + let rootPath = File.from(repo.path).relativize(File.cwd()); + if (repo.isRoot) { + rootPath = true; + } + me.log.debug(`Writing manifest to '${manifest.path}'`); + manifest.save({ root: rootPath }); + } +} + +install.define({ + help: { + '': 'Retrieves remote repositories and installs local dependencies', + 'forks': 'Obey local fork settings when downloading repos' + }, + switches: '[forks:boolean=true]' +}); + +module.exports = install; diff --git a/index.js b/index.js index 6bffcac..15e61c0 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ module.exports = { - Repo: require('./src/Repo'), - Package: require('./src/Package') + Repo: require('./src/repo'), + Package: require('./src/package') }; diff --git a/package.json b/package.json index e226739..c556579 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,61 @@ { - "name": "mondorepo", - "version": "1.0.0-alpha.0", - "description": "Management for collections of packages across teams", - "main": "index.js", - "bin": { - "mondo": "./bin/mondo.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/sencha/mondorepo.git" - }, - "scripts": { - "test": "mocha ./test/specs/**/*.js", - "test-debug": "devtool node_modules/mocha/bin/_mocha -qc --break -- ./test/specs/**/*.js", - "lint": "eslint ./src/**/*.js" - }, - "keywords": [ - "multimodule", - "repository", - "mondo", - "distributed", - "modular", - "packages", - "monorepo" - ], - "author": "Sencha", - "license": "MIT", - "eslintConfig": ".eslintrc.json", - "devDependencies": { - "assertly": "^1.0.0-beta.2", - "eslint": "^3.5.0", - "mocha": "^3.0.2" - }, - "dependencies": { - "chalk": "^1.1.3", - "columnify": "^1.5.4", - "deep-extend": "^0.4.1", - "glob": "^7.0.6", - "hash-files": "^1.1.1", - "json5": "^0.5.1", - "jsonfile": "^2.4.0", - "mkdirp": "latest", - "ora": "^0.3.0", - "semver": "^5.3.0", - "simple-git": "^1.52.0", - "switchit": "^1.0.7" - }, - "bugs": { - "url": "https://github.com/sencha/mondorepo/issues" - }, - "homepage": "https://github.com/sencha/mondorepo#readme", - "directories": { - "test": "test" - } + "name": "mondorepo", + "version": "1.0.0-alpha.0", + "description": "Management for collections of packages across teams", + "main": "index.js", + "bin": { + "mondo": "./bin/mondo.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sencha/mondorepo.git" + }, + "scripts": { + "test": "nyc mocha ./test/specs/**/*.js", + "test-debug": "devtool node_modules/mocha/bin/_mocha -qc --break -- ./test/specs/**/*.js", + "lint": "eslint ./cli/**/*.js ./src/**/*.js" + }, + "keywords": [ + "multimodule", + "repository", + "mondo", + "distributed", + "modular", + "packages", + "monorepo", + "multirepo" + ], + "author": "Sencha, Inc.", + "license": "MIT", + "eslintConfig": ".eslintrc.json", + "devDependencies": { + "assertly": "^1.0.0-beta.2", + "eslint": "^3.19.0", + "mocha": "^3.5.0", + "nyc": "^11.1.0" + }, + "dependencies": { + "chalk": "^2.1.0", + "columnify": "^1.5.4", + "conf": "^1.1.2", + "glob": "^7.1.2", + "hash-files": "^1.1.1", + "json5": "^0.5.1", + "jsonfile": "^2.4.0", + "loog": "^1.6.0", + "phylo": "^1.0.0-rc.1", + "semver": "^5.4.1", + "simple-git": "^1.75.0", + "switchit": "^1.0.8" + }, + "bugs": { + "url": "https://github.com/sencha/mondorepo/issues" + }, + "homepage": "https://github.com/sencha/mondorepo#readme", + "directories": { + "test": "test" + }, + "engines": { + "npm": ">=5.0.0" + } } diff --git a/src/Errors.js b/src/Errors.js deleted file mode 100644 index 27c09cd..0000000 --- a/src/Errors.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - RepositoryNotFound: { - code: 100, - message: 'Unable to find repository ${%1}' - } -}; diff --git a/src/PackageManagers.js b/src/PackageManagers.js deleted file mode 100644 index f96cc74..0000000 --- a/src/PackageManagers.js +++ /dev/null @@ -1,21 +0,0 @@ -const Npm = require('./pkgMgrs/Npm'); -const Yarn = require('./pkgMgrs/Yarn'); - -class PackageManagers { - static configure (opts) { - PackageManagers._opts = opts; - } - - static registerPackageManager (name, packageManager) { - let me = PackageManagers; - me._managers[name] = packageManager; - me[name] = () => new me._managers[name](me._opts); - } -} - -PackageManagers._managers = {}; - -PackageManagers.registerPackageManager('npm', Npm); -PackageManagers.registerPackageManager('yarn', Yarn); - -module.exports = PackageManagers; \ No newline at end of file diff --git a/src/RequireHooker.js b/src/RequireHooker.js deleted file mode 100644 index 3152442..0000000 --- a/src/RequireHooker.js +++ /dev/null @@ -1,23 +0,0 @@ -class RequireHooker { - - /** - * @param resolver - */ - static hook(resolver) { - let _require = module.constructor.prototype.require; - - module.constructor.prototype.require = function(id) { - id = resolver.resolve(id); - return _require.call(this, id); - }; - - // This is how we can hook module search paths, if we would like to add more paths - // to search for modules just contact to the paths variable - // _findPath = Module._findPath, - /*Module._findPath = function (request, paths, isMain) { - return _findPath.call(this, request, paths, isMain); - }*/ - } -} - -module.exports = RequireHooker; diff --git a/src/Resolver.js b/src/Resolver.js deleted file mode 100644 index fc6f473..0000000 --- a/src/Resolver.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; -const Path = require('path'); -const deepExtend = require('deep-extend'); - -class Resolver { - constructor() { - this.config = { - resolve: { - alias: {} - } - }; - } - - alias(id) { - let aliases = this.config.resolve.alias; - let slash = id.indexOf('/'); - let scopeTest = id.match(/^(@.*?\/.*)/); - let scopePathTest = id.match(/^(@.*?\/.*?)\/(.*)/); - - if (scopePathTest) { //Scoped package with a path to a file - let packageName = scopePathTest[1]; - let value = aliases[packageName]; - id = Path.resolve(value, scopePathTest[2]); - } else if (slash < 0 || scopeTest) { - let value = aliases[id] || aliases[`${id}$`]; - if (value) { - return value; - } - } else { - let packageName = id.substr(0, slash); - let value = aliases[packageName]; - - if (value) { - id = Path.resolve(value, id.substr(slash + 1)); - } - } - return id; - } - - - addAliases(aliases) { - deepExtend(this.config.resolve.alias, aliases); - } - - resolve(id) { - id = this.alias(id); - return id; - } -} - -module.exports = Resolver; diff --git a/src/Util.js b/src/Util.js deleted file mode 100644 index c5fc871..0000000 --- a/src/Util.js +++ /dev/null @@ -1,57 +0,0 @@ -const Util = { - clone (item) { - if (item == null) { - return item; - } - - const type = Object.prototype.toString.call(item); - let clone = item; - let i, key; - - if (type === '[object Date]') { - clone = new Date(item.getTime()); - } - else if (type === '[object Array]') { - clone = []; - - for (i = item.length; i--; ) { - clone[i] = Util.clone(item[i]); - } - } - else if (type === '[object Object]' && item.constructor === Object) { - clone = {}; - - for (key in item) { - clone[key] = Util.clone(item[key]); - } - } - - return clone; - }, - - merge (destination, source) { - if (source) { - for (let key in source) { - let value = source[key]; - - if (value && value.constructor === Object) { - let sourceKey = destination[key]; - - if (sourceKey && sourceKey.constructor === Object) { - Util.merge(sourceKey, value); - } - else { - destination[key] = Util.clone(value); - } - } - else { - destination[key] = value; - } - } - } - - return destination; - } -}; - -module.exports = Util; diff --git a/src/cli.js b/src/cli.js deleted file mode 100644 index ac489ea..0000000 --- a/src/cli.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -const Path = require('path'); - -const JSON5 = require('json5'); - -const {Container, commands} = require('switchit'); -const Install = require('./commands/Install'); -const Exec = require('./commands/Exec'); -const Fork = require('./commands/Fork'); -const Publish = require('./commands/Publish'); -const Rev = require('./commands/Rev'); -const Logger = require('./utils/Logger'); -const FileUtil = require('./utils/FileUtil'); -const constants = require('./constants'); -const Util = require('./Util'); - -const defaultSettings = { - forks: { - // - } -}; - -if (commands.Version) { - commands.Version.home = Path.resolve(__dirname, '..'); -} - -class Mondo extends Container { - beforeExecute(params) { - super.beforeExecute(params); - - let me = this; - - Logger.setThreshold(params.verbose ? 'debug' : (params.quiet ? 'warn' : 'info')); - - me.settingsPath = Path.resolve(constants.home, constants.settings); - - let settings; - - if (FileUtil.exists(me.settingsPath)) { - Logger.debug(`Loading settings file from ${me.settingsPath}`); - settings = JSON5.parse(FileUtil.getFileContents(me.settingsPath)); - } - - me.settings = Util.merge(Util.merge({}, defaultSettings), settings); - } -} - -Mondo.define({ - help: { - '': 'Management for collections of packages across teams', - quiet: 'Provide less logging output (remove info)', - verbose: 'Provide verbose logging output' - }, - switches: '[quiet:boolean=false] [verbose:boolean=false]', - commands: { - help: commands.Help, - install: Install, - exec: Exec, - rev: Rev, - publish: Publish, - version: commands.Version, - fork: Fork - } -}); - -module.exports = Mondo; diff --git a/src/utils/Collection.js b/src/collection.js similarity index 100% rename from src/utils/Collection.js rename to src/collection.js diff --git a/src/commands/Exec.js b/src/commands/Exec.js deleted file mode 100644 index 1d9f153..0000000 --- a/src/commands/Exec.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -const {Command} = require('switchit'); -const spawn = require('child_process').spawn; -const Path = require('path'); - -class Exec extends Command { - execute(params) { - const file = params.file; - if (params.debug) { - let args = ['--require', `${Path.resolve(__dirname, '..', 'init.js')}`, file]; - let devtool = spawn('devtool', args); - - devtool.stdout.pipe(process.stdout); - devtool.stderr.pipe(process.stderr); - devtool.on('close', (code) => { - process.exit(code); - }); - } else { - require(Path.resolve(__dirname, '..', 'init')); - require(Path.resolve(process.cwd(), file)); - } - } -} - -Exec.define({ - help: { - '': 'Executes a .js file using `devtool`', - 'file': 'The file to run' - }, - parameters: '[file=]', - switches: '[debug:boolean=false]' -}); - - -module.exports = Exec; - -// TODO: Add all params for devtool -/* - --watch, -w enable file watching (for development) - --quit, -q quit application on fatal errors - --console, -c redirect console logs to terminal - --index, -i specify a different index.html file - --poll, -p enable polling when --watch is given - --show, -s show the browser window (default false) - --headless, -h do not open the DevTools window - --timeout, -t if specified, will close after X seconds - --break insert a breakpoint in entry point - --config a path to .devtoolrc config file - --verbose verbose Chromium logging - --version, -v log versions of underlying tools - --require, -r require path(s) before running entry - --browser-field, --bf resolve using "browser" field - --no-source-maps, - --no-sm disable source map generation - --no-node-timers, - --no-nt use browser timers - --no-browser-globals, - --no-bg removes window,document,navigator from required files - */ diff --git a/src/commands/Fork/Remove.js b/src/commands/Fork/Remove.js deleted file mode 100644 index f83c1cd..0000000 --- a/src/commands/Fork/Remove.js +++ /dev/null @@ -1,34 +0,0 @@ -const Command = require('switchit').Command; -const chalk = require('chalk'); - -const Logger = require('../../utils/Logger'); -const FileUtils = require('../../utils/FileUtil'); - -class Remove extends Command { - execute (params) { - let me = this; - let mondo = me.root(); - - let forks = mondo.settings.forks; - - if (!forks[params.repoName]) { - Logger.info(`There is no known fork for ${chalk.bold.yellow(params.repoName)}.`); - return; - } - - let old = forks[params.repoName]; - delete forks[params.repoName]; - FileUtils.writeFile(mondo.settingsPath, JSON.stringify(mondo.settings, null, ' ')); - Logger.info(`Removed ${chalk.bold.yellow(old)} as known fork for ${chalk.bold.yellow(params.repoName)}.`); - } -} - -Remove.define({ - help: { - '': 'Removes the known fork for a repository', - repoName: 'The name of the repository' - }, - parameters: '{repoName}' -}); - -module.exports = Remove; \ No newline at end of file diff --git a/src/commands/Fork/index.js b/src/commands/Fork/index.js deleted file mode 100644 index e33af47..0000000 --- a/src/commands/Fork/index.js +++ /dev/null @@ -1,20 +0,0 @@ -const Container = require('switchit').Container; - -const Add = require('./Add'); -const List = require('./List'); -const Remove = require('./Remove'); - -class Fork extends Container { -} - -Fork.define({ - help: 'Commands to manage the global set of known forks', - commands: { - add: Add, - list: List, - remove: Remove, - '': List - } -}); - -module.exports = Fork; \ No newline at end of file diff --git a/src/commands/Install.js b/src/commands/Install.js deleted file mode 100644 index b391c28..0000000 --- a/src/commands/Install.js +++ /dev/null @@ -1,220 +0,0 @@ -"use strict"; -const fs = require('fs'); -const Path = require('path'); - -const chalk = require('chalk'); -const {Command} = require('switchit'); -const ora = require('ora'); - -const constants = require('../constants'); -const Repo = require('../Repo'); -const VCS = require('../VCS'); -const PackageManagers = require('../PackageManagers'); -const Logger = require('../utils/Logger'); -const FileUtil = require('../utils/FileUtil'); - -const isWindows = /^win/.test(process.platform); - -class Install extends Command { - execute(params) { - let me = this; - let mondo = this.root(); - - me.binDirs = []; - me.wrappedBins = []; - - me.vcs = VCS.git({ - forks: params.forks ? mondo.settings.forks || {} : {} - }); - - if (!me.vcs.available()) { - throw new Error('Git is required to run `mondo install`'); - } - - me.packageManager = (PackageManagers[mondo.settings.packageManager] || PackageManagers.yarn)(); - - // Fallback to NPM when any package manager is not available - if (!me.packageManager.available()) { - Logger.debug('Global yarn not found, using npm instead.'); - me.packageManager = PackageManagers.npm(); - } - - const repo = Repo.open(process.cwd()); - return me.installRepo(repo.root).then(() => { - if (me.wrappedBins.length > 0) { - let message = 'Linking local binaries'; - Logger.debug(message); - if (me.spinner) { - me.spinner.succeed(); - me.spinner.text = message; - me.spinner.start(); - } - - let createWinBinary = (binDir, wrappedBin) => { - let binPath = Path.join(binDir, wrappedBin.name + '.cmd'); - let message = `- Creating ${chalk.green(binPath)}`; - - if (fs.existsSync(binPath)) { - message = `- Binary ${chalk.green(binPath)} already exists, overwriting`; - if (!me.spinner) { - Logger.warn(message); - } - } - - Logger.debug(message); - fs.writeFileSync(binPath, `@IF EXIST "%~dp0\\node.exe" (\n "%~dp0\\node.exe" "%~dp0\\${wrappedBin.name}" %*\n) ELSE (\n @SETLOCAL\n @SET PATHEXT=%PATHEXT:;.JS;=;%\n node "%~dp0\\${wrappedBin.name}" %*\n)\n`); - fs.chmodSync(binPath, '755'); - }; - - let createUnixBinary = (binDir, wrappedBin) => { - let binPath = Path.join(binDir, wrappedBin.name); - let message = `- Creating ${chalk.green(binPath)}`; - - if (fs.existsSync(binPath)) { - message = `- Binary ${chalk.green(binPath)} already exists, overwriting`; - if (!me.spinner) { - Logger.warn(message); - } - } - - Logger.debug(message); - fs.writeFileSync(binPath, `#! /usr/bin/env node\nrequire('mondorepo/src/init');\nrequire('${wrappedBin.pkg.name}/${wrappedBin.file}');\n`); - fs.chmodSync(binPath, '755'); - }; - - me.wrappedBins.forEach(function(wrappedBin) { - let message = `- Linking '${chalk.green(wrappedBin.name)}'`; - Logger.debug(message); - if (me.spinner) { - me.spinner.succeed(); - me.spinner.text = message; - me.spinner.start(); - } - me.binDirs.forEach(function(binDir) { - if (!fs.existsSync(binDir)) { - FileUtil.mkdirp(binDir); - } - createUnixBinary(binDir, wrappedBin); - if (isWindows) { - createWinBinary(binDir, wrappedBin); - } - }); - }); - } - if (me.spinner) { - me.spinner.succeed(); - } - }); - } - - installRepo(repo) { - let me = this; - if (repo.installed) { - return Promise.resolve(repo); - } - - repo.installed = true; - - if (repo.exists()) { - if (repo.isRoot) { - const childPath = Path.resolve(repo.path, constants.child); - fs.writeFileSync(childPath, JSON.stringify({root: true})); - } - - return me.installRepoPackages(repo); - } - - let message = chalk.cyan(`Cloning repository '${chalk.magenta(repo.name)}' from '${chalk.yellow(repo.source.repository)}#${chalk.magenta(repo.source.branch || constants.branch)}' into '${chalk.magenta(Path.relative(process.cwd(), repo.path))}'`); - Logger.debug(message); - if (me.spinner) { - me.spinner.succeed(); - me.spinner.text = message; - me.spinner.start(); - } - - - - return me.vcs.clone(repo.source.repository, repo.path, repo.source.branch).then(() => { - fs.writeFileSync(Path.join(repo.path, constants.child), JSON.stringify({root: Path.relative(repo.path, process.cwd())})); - return me.installRepoPackages(repo); - }); - } - - installChildren(repo) { - let me = this; - let uses; - - for (let child of repo.uses) { - if (uses) { - uses = uses.then(() => me.installRepo(child)); - } else { - uses = me.installRepo(child); - } - } - - if (uses) { - return uses.then(() => repo); - } - - return Promise.resolve(repo); - } - - installRepoPackages(repo) { - let me = this; - repo.open(); - - let message = `Installing packages for '${chalk.yellow(repo.name)}'`; - Logger.debug(message); - if (!Logger.debug.enabled) { - if (me.spinner) { - me.spinner.succeed(); - } else { - me.spinner = ora(); - } - me.spinner.text = message; - me.spinner.start(); - } - let install = me.packageManager.install(repo.path); - me.binDirs.push(Path.join(repo.path, 'node_modules', '.bin')); - let packages = repo.packages; - for (let pkg of packages) { - install = install.then(() => { - let message = `- Installing '${chalk.yellow(repo.name)}:${chalk.magenta(pkg.name)}'`; - Logger.debug(message); - if (me.spinner) { - me.spinner.succeed(); - me.spinner.text = message; - me.spinner.start(); - } - - return me.packageManager.install(pkg.path).then(() => { - me.binDirs.push(Path.join(pkg.path, 'node_modules', '.bin')); - let pkgJson = require(Path.join(pkg.path, 'package.json')); - if (pkgJson.bin) { - Object.keys(pkgJson.bin).forEach(function(name) { - me.wrappedBins.push({ - pkg: pkg, - name: name, - file: pkgJson.bin[name] - }); - }); - } - }); - }); - } - - // read the repo from the disk - return install.then(() => me.installChildren(repo)); - } - -} - -Install.define({ - help: { - '': 'Brings the mondo in!', - 'forks': 'Enable local fork settings when downloading repos' - }, - switches: '[forks:boolean=true]' -}); - -module.exports = Install; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..09a14a3 --- /dev/null +++ b/src/config.js @@ -0,0 +1,62 @@ +const Conf = require('conf'); + +class Config { + constructor () { + this._transient = {}; + this._store = new Conf({ + projectName: Config.projectName, + defaults: { + manifest: 'package.json', + child: '.mondo.json', + install: 'mondo_repos', + packages: ['.'], + branch: 'master', + type: 'github', + repo: 'repo', + forkedRepoName: 'upstream' + } + }); + } + + get (key, defaultValue) { + if (Config.transientProps.indexOf(key.split('.')[0]) > -1) { + return this._transient[key] || defaultValue; + } else { + return this._store.get(key, defaultValue); + } + } + + set (key, value) { + if (!value) { + this._store.set(key); + } else { + + if (Config.transientProps.indexOf(key.split('.')[0]) > -1) { + this._transient[key] = value; + } else { + this._store.set(key, value); + } + } + } + + has (key) { + if (Config.transientProps.indexOf(key.split('.')[0]) > -1) { + return this._transient.hasOwnProperty(key); + } else { + return this._store.has(key); + } + } + + delete (key) { + if (Config.transientProps.indexOf(key.split('.')[0]) > -1) { + delete this._transient[key]; + } else { + this._store.delete(key); + } + } +} + +Config.projectName = 'mondo'; +Config.transientProps = ['pkg']; + +module.exports = Config; \ No newline at end of file diff --git a/src/constants.js b/src/constants.js deleted file mode 100644 index 86e2584..0000000 --- a/src/constants.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; -const Path = require('path'); -const os = require('os'); -module.exports = { - manifest: 'package.json', - child: '.mondo.json', - install: 'mondo_repos', - packages: ['.'], - branch: 'master', - type: 'github', - repo: 'repo', - home: Path.resolve(os.homedir(), '.mondo'), - settings: 'settings.json', - forkedRepoName: 'upstream' -}; diff --git a/src/Graph.js b/src/graph.js similarity index 88% rename from src/Graph.js rename to src/graph.js index 9b0ac1b..11ee4ac 100644 --- a/src/Graph.js +++ b/src/graph.js @@ -1,5 +1,5 @@ -const Collection = require('./utils/Collection.js'); -const Logger = require('./utils/Logger.js'); +const Collection = require('./collection.js'); +const console = require('loog'); class Graph { constructor(item) { @@ -17,7 +17,7 @@ class Graph { _descend(item) { if (this._map[item.name] === 1) { - Logger.error(`${item.name} was found multiple times`); + console.error(`${item.name} was found multiple times`); // issue we are here again error time // use _stack to show path to problem } diff --git a/src/init.js b/src/init.js deleted file mode 100644 index 9707559..0000000 --- a/src/init.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; -const Repo = require('./Repo'); -const Resolver = require('./Resolver'); -const RequireHooker = require('./RequireHooker'); -const repo = Repo.open(process.cwd()); -const resolver = new Resolver(); - -resolver.addAliases(repo.allPackageAliases); -RequireHooker.hook(resolver); diff --git a/src/npm.js b/src/npm.js new file mode 100644 index 0000000..da81a98 --- /dev/null +++ b/src/npm.js @@ -0,0 +1,62 @@ +const chalk = require('chalk'); +const spawn = require('child_process').spawn; +const File = require('phylo'); + +const isWindows = /^win/.test(process.platform); +const npm = `npm${isWindows ? '.cmd' : ''}`; + +class Npm { + constructor (opts) { + Object.assign(this, opts); + } + + spawn(args, options) { + return new Promise((resolve, reject) => { + let process = spawn(npm, args, options); + + process.on('close', (code) => { + if (code) { + reject(new Error(`NPM ${args.join(' ')} exited with code: ${code}`)); + } else { + resolve(); + } + }); + + process.on('error', reject); + }); + } + + install (cwd, opts) { + cwd = File.from(cwd); + opts = opts || {}; + + let me = this; + let args = ['install']; + // TODO: Revisit this once https://github.com/npm/npm/issues/17257 is dealt with + args.push('--no-shrinkwrap'); + if ('save' in opts) { + args.push(`--${!opts.save ? 'no-' : ''}save${!!opts.save ? `-${opts.save === true ? 'prod' : opts.save}` : ''}`); + } + if ('pkg' in opts) { + args = args.concat(opts.pkg.split(' ')); + } + me.log.debug(`Running 'npm ${args.join(' ')}' at ${cwd}`); + return me.spawn(args, { + cwd: cwd.path, + stdio: me.debug ? 'inherit' : 'pipe' + }); + } + + view(name, version) { + const pkg = name + (version !== undefined ? `@${version}` : ''); + return this.spawn(['view', pkg, '--json']); + } + + publish(path) { + return this.spawn(['publish'], { + cwd: path + }); + } +} + +module.exports = Npm; diff --git a/src/Package.js b/src/package.js similarity index 56% rename from src/Package.js rename to src/package.js index b39bd76..fb95ee2 100644 --- a/src/Package.js +++ b/src/package.js @@ -1,28 +1,27 @@ -const Path = require('path'); -const Collection = require('./utils/Collection.js'); -const FileUtil = require('./utils/FileUtil.js'); -const Graph = require('./Graph.js'); +const File = require('phylo'); + +const Collection = require('./collection.js'); +const Graph = require('./graph.js'); const glob = require('glob'); const hashFiles = require('hash-files'); const semver = require('semver'); const JSON5 = require('json5'); class Package { - constructor(packageFile, repo) { - packageFile = FileUtil.absolute(packageFile); + packageFile = File.from(packageFile).absolutify(); - if (!FileUtil.isFile(packageFile)) { - packageFile = Path.resolve(packageFile, 'package.json'); + if (!packageFile.isFile()) { + packageFile = packageFile.join('package.json'); } this._packageFile = packageFile; - this._packagePath = Path.dirname(packageFile); - this._package = JSON5.parse(FileUtil.getFileContents(packageFile)) || {}; + this._packagePath = packageFile.parent; + this._package = packageFile.load() || {}; this._mondo = (this._package && this._package.mondo) || {}; - this._basePath = Path.resolve(this.path, this._mondo.base || '.'); + this._basePath = this.path.join(this._mondo.base || '.'); if (!this._package.version) { - throw new Error(`Package '${this._package.name}' requires a version`); + this._package.version = '0.0.0'; } this._version = semver(this._package.version); this._repo = repo; @@ -58,7 +57,7 @@ class Package { get hash() { if (!this._hash) { - const files = glob.sync(Path.join(this.path, '**'), {ignore: ['**/node_modules/**/*']}); + const files = glob.sync(this.path.join('**').path, {ignore: ['**/node_modules/**/*']}); this._hash = hashFiles.sync({files: files}); } @@ -66,27 +65,15 @@ class Package { } get mondoDependencies() { - let mondoDependencies = this._mondoDependencies; - - if (!mondoDependencies) { - const deps = this._mondo.dependencies || {}; - const visiblePackages = this.repo.visiblePackages; - mondoDependencies = new Collection(); - - Object.keys(deps).forEach(depName => { - const pkg = visiblePackages.get(depName); - - if (!pkg) { - throw new Error(`Package ${depName} was not found from package ${this.name}`); - } - - mondoDependencies.add(pkg); - }); - - this._mondoDependencies = mondoDependencies; + if (!this._mondoDependencies) { + const deps = Object.keys(this._package.dependencies || {}); + this._mondoDependencies = new Collection(); + this._mondoDependencies.addAll( + this.repo.root.allPackages + .filter(pkg => !!~deps.indexOf(pkg.name) && !pkg.path.equals(this.path)) + ); } - - return mondoDependencies; + return this._mondoDependencies; } get allMondoDependencies() { diff --git a/src/pkgMgrs/Base.js b/src/pkgMgrs/Base.js deleted file mode 100644 index 6eeb0aa..0000000 --- a/src/pkgMgrs/Base.js +++ /dev/null @@ -1,14 +0,0 @@ -class PackageManager { - constructor (opts) { - Object.assign(this, opts); - } - - install (path) { - throw new Error("Not yet implemented"); - } - - available() { - return true; - } -} -module.exports = PackageManager; diff --git a/src/pkgMgrs/Npm.js b/src/pkgMgrs/Npm.js deleted file mode 100644 index 74e9bcd..0000000 --- a/src/pkgMgrs/Npm.js +++ /dev/null @@ -1,74 +0,0 @@ -const chalk = require('chalk'); -const spawn = require('child_process').spawn; - -const PackageManager = require('./Base'); -const Logger = require('../utils/Logger'); - -const isWindows = /^win/.test(process.platform); -const npm = `npm${isWindows ? '.cmd' : ''}`; - -class Npm extends PackageManager { - spawn(args, options) { - return new Promise((resolve, reject) => { - let process = spawn(npm, args, options); - - let result = ''; - process.stdout.on('data', function(data) { - result += data.toString(); - }); - - process.stderr.on('data', function(data) { - result += data.toString(); - }); - - process.on('close', (code) => { - if (code) { - reject(new Error(`NPM ${args.join(' ')} exited with code: ${code}:\n${result}`)); - } else { - resolve(result); - } - }); - - process.on('error', reject); - }); - } - - install(path) { - const me = this; - const args = ['install']; - const opts = {cwd: path}; - let install = ''; - - if (Logger.debug.enabled) { - opts['stdio'] = 'inherit'; - return me.spawn(args, opts); - } - - args.push('--depth'); - args.push('0'); - return me.spawn(['set', 'progress=false'], opts) - .then(() => { - return me.spawn(args, opts); - }).then(result => { - install = result; - Logger.debug(result.trim()); - return me.spawn(['set', 'progress=true']); - }).then(() => { - return install; - }); - } - - view(name, version) { - const pkg = name + (version !== undefined ? `@${version}` : ''); - const args = ['view', pkg, '--json']; - return this.spawn(args); - } - - publish(path) { - const opts = {cwd: path}; - const args = ['publish']; - return this.spawn(args, opts); - } -} - -module.exports = Npm; diff --git a/src/pkgMgrs/Yarn.js b/src/pkgMgrs/Yarn.js deleted file mode 100644 index 43b6bcb..0000000 --- a/src/pkgMgrs/Yarn.js +++ /dev/null @@ -1,56 +0,0 @@ -const chalk = require('chalk'); -const path = require('path'); -const spawn = require('child_process').spawn; -const exec = require('child_process').execSync; - -const PackageManager = require('./Base'); -const Logger = require('../utils/Logger'); - -const isWindows = /^win/.test(process.platform); - -class Yarn extends PackageManager { - spawn(args, options) { - return new Promise((resolve, reject) => { - let process = spawn(`yarn${isWindows ? '.cmd' : ''}`, args, options); - - let result = ''; - if (!Logger.debug.enabled) { - process.stdout.on('data', function(data) { - result += data.toString(); - }); - } - - process.on('close', (code) => { - if (code) { - reject(`Yarn install exited with code: ${code}`); - } else { - Logger.debug(result.trim()); - resolve(); - } - }); - - process.on('error', reject); - }); - } - - install(path) { - let me = this; - let args = ['install']; - let opts = {cwd: path}; - if (Logger.debug.enabled) { - opts['stdio'] = 'inherit'; - } - return me.spawn(args, opts); - } - - available() { - try { - exec('yarn --version'); - return true; - } catch (e) { - return false; - } - } -} - -module.exports = Yarn; diff --git a/src/Repo.js b/src/repo.js similarity index 54% rename from src/Repo.js rename to src/repo.js index 204b92c..926bd5f 100644 --- a/src/Repo.js +++ b/src/repo.js @@ -1,44 +1,58 @@ -"use strict"; +const File = require('phylo'); +const Config = require('./config'); + const Path = require('path'); -const jsonfile = require('jsonfile'); const glob = require("glob"); -const Package = require('./Package'); -const Collection = require('./utils/Collection.js'); -const Graph = require('./Graph'); -const constants = require('./constants.js'); -const FileUtil = require('./utils/FileUtil.js'); -const cwd = process.cwd(); +const Package = require('./package'); +const Collection = require('./collection.js'); +const Graph = require('./graph'); class Repo { - + //---------------------- Static Methods ----------------------// /** * @param manifestPath * @private */ - static getRepoPath(manifestPath) { - let parentDirectory = Path.resolve(manifestPath, '..'); - let manifestFile = Path.resolve(manifestPath, constants.manifest); - let childFile = Path.resolve(manifestPath, constants.child); + static getRepoPath(manifestPath, cfg = new Config()) { + let log = Repo.getLogger(); + let manifestDir = File.from(manifestPath); + + if (!manifestDir) { + return null; + } + + log.debug(`Looking at ${manifestDir.path}`); + log.indent(); + let parentDirectory = manifestDir.parent; + let manifestFile = manifestDir.join(cfg.get('manifest')); + let childFile = manifestDir.join(cfg.get('child')); // If there is a .mondo.json you are a Repo for sure - if (FileUtil.exists(childFile)) { + if (childFile.exists()) { + log.debug(`Found repo at ${childFile.parent}`); + log.outdent(); return manifestPath; } // check the package.json for declaration of mondo repo'ness - if (FileUtil.exists(manifestFile)) { - let json = jsonfile.readFileSync(manifestFile); + if (manifestFile.exists()) { + log.debug(`Found candidate manifest file: ${manifestFile.path}`); + let json = manifestFile.load(); let mondo = json.mondo || {}; - if (mondo[constants.repo]) { + if (mondo[cfg.get('repo')]) { + log.debug(`Repo found at ${manifestPath.path}`) + log.outdent(); return manifestPath; } } if (parentDirectory === manifestPath) { + log.outdent(); return null; } else { - return Repo.getRepoPath(parentDirectory); + log.outdent(); + return Repo.getRepoPath(parentDirectory, cfg); } } @@ -46,37 +60,60 @@ class Repo { * @param {String} [repoPath] Optional path to open. Defaults to the current working directory. * @returns {Repo} */ - static open(repoPath = cwd) { - repoPath = FileUtil.absolute(repoPath); - repoPath = Repo.getRepoPath(repoPath); + static open(repoPath = File.cwd(), cfg = new Config()) { + let log = Repo.getLogger(); + repoPath = File.from(repoPath).absolutify(); + log.debug(`Looking for a repo, starting from ${repoPath.path}`); + log.indent(); + repoPath = Repo.getRepoPath(repoPath, cfg); + log.outdent(); if (repoPath == null) { throw new Error('Repository not found. Are you missing the `repo: true` config?'); } - let repo = new Repo({path: repoPath}); + let repo = new Repo({path: repoPath, config: cfg}); repo.open(); return repo; } + static getLogger() { + if (!Repo.log) { + Repo.log = require('loog')({ + prefixStyle: 'ascii' + }); + } + return Repo.log; + } + + //---------------------- Constructor ----------------------// + constructor(config) { this._registry = {}; + if (!config.hasOwnProperty('config')) { + config.config = new Config(); + } + this.config = config.config; + this.log = Repo.getLogger(); Object.assign(this, config); } + //---------------------- Getters and Setters ----------------------// + /** * @property {Package[]} allPackages */ get allPackages() { - let allPackages = this._allPackages; + let me = this; + let allPackages = me._allPackages; if (!allPackages) { - allPackages = this.packages.clone(); - let repos = this.uses; + allPackages = me.packages.clone(); + let repos = me.uses; for (let repo of repos) { allPackages.addAll(repo.allPackages); } - this._allPackages = allPackages; + me._allPackages = allPackages; } return allPackages; @@ -86,13 +123,14 @@ class Repo { * @property {Object[]} allPackageAliases */ get allPackageAliases() { - let allPackageAliases = this._allPackageAliases; + let me = this; + let allPackageAliases = me._allPackageAliases; if (!allPackageAliases) { - let allPackages = this.allPackages; - allPackageAliases = this._allPackageAliases = {}; + let allPackages = me.allPackages; + allPackageAliases = me._allPackageAliases = {}; allPackages.forEach(pkg => { - this._allPackageAliases[pkg.name] = pkg.base; + me._allPackageAliases[pkg.name] = pkg.base; }); } @@ -103,10 +141,11 @@ class Repo { * @property {Repo[]} allRepos */ get allRepos() { - let allRepos = this._allRepos; + let me = this; + let allRepos = me._allRepos; if (!allRepos) { - if (this.isRoot) { + if (me.isRoot) { allRepos = new Collection(); let getAllUses = (repo) => { let uses = repo.uses; @@ -120,10 +159,10 @@ class Repo { } }; - getAllUses(this); - this._allRepos = allRepos; + getAllUses(me); + me._allRepos = allRepos; } else { - let root = this.root; + let root = me.root; return root.allRepos; } } @@ -135,10 +174,11 @@ class Repo { * @property {String} installDir */ get installDir() { - let root = this.root; + let me = this; + let root = me.root; if (root && root.manifest) { - return Path.resolve(root.path, root.manifest.install || constants.install); + return root.path.join(root.manifest.install || me.config.get('install')); } else { throw new Error(`Unable to find root for Repositories or Root manifest is not set.`); } @@ -162,20 +202,16 @@ class Repo { * @property {Object} mondo */ get mondo() { - if (this.manifest) { - return this.manifest.mondo; - } + return this.manifest ? this.manifest.mondo : undefined; } /** * @property {String} name */ set name(name) { - if (this._name && this._name !== name) { - throw new Error(`Inconsistent name for ${name}`); + if (name) { + this._name = name; } - - this._name = name; } get name() { @@ -186,15 +222,16 @@ class Repo { * @property {Package[]} packages */ get packages() { - let packages = this._packages; + let me = this; + let packages = me._packages; if (!packages) { - let manifest = this.manifest; + let manifest = me.manifest; if (manifest) { - let mondo = this.mondo || {}; - let directories = mondo.packages === false ? [] : mondo.packages || Array.from(constants.packages); - let manifestPath = this.path; + let mondo = me.mondo || {}; + let directories = mondo.packages === false ? [] : mondo.packages || Array.from(me.config.get('packages')); + let manifestPath = me.path; if (!Array.isArray(directories)) { directories = [directories]; @@ -203,22 +240,22 @@ class Repo { packages = new Collection(); for (let packageDir of directories) { let npmPackagesPaths; - packageDir = Path.resolve(manifestPath, packageDir); + packageDir = manifestPath.join(packageDir); // test for self package root, allows for only one package - if (manifestPath === packageDir) { - npmPackagesPaths = [Path.resolve(packageDir, 'package.json')]; + if (manifestPath.equals(packageDir)) { + npmPackagesPaths = [packageDir.join('package.json')]; } else { - npmPackagesPaths = glob.sync(`${packageDir}/**/package.json`, {ignore: '**/node_modules/**/*'}); + npmPackagesPaths = packageDir.tips('package.json'); } for (let npmPackagePath of npmPackagesPaths) { - let npmPackage = new Package(npmPackagePath, this); + let npmPackage = new Package(npmPackagePath, me); packages.add(npmPackage); } } - this._packages = packages; + me._packages = packages; } else { throw new Error(`Unable to get packages from a repo without path information. Configure 'path' for ${this.name}`); } @@ -228,20 +265,21 @@ class Repo { } /** - * @property {String} path + * @property {File} path */ get path() { return this._manifestPath; } set path(path) { - let manifestPath = this._manifestPath = FileUtil.absolute(path); + let me = this; + let manifestPath = me._manifestPath = File.from(path).absolutify(); - if (Path.basename(manifestPath) === constants.manifest) { - this._manifestPath = Path.dirname(manifestPath); - this._manifestFile = manifestPath; + if (manifestPath.name === me.config.get('manifest')) { + me._manifestPath = manifestPath.parent; + me._manifestFile = manifestPath; } else { - this._manifestFile = Path.resolve(manifestPath, constants.manifest); + me._manifestFile = manifestPath.join(me.config.get('manifest')); } } @@ -249,28 +287,29 @@ class Repo { * @property {Repo} root */ get root() { - let root = this._root; + let me = this; + let root = me._root; if (!root) { - let manifestPath = this.path; - let mondoDescriptorFile = Path.resolve(manifestPath, constants.child); + let manifestPath = me.path; + let mondoDescriptorFile = manifestPath.join(me.config.get('child')); // Does a mondo descriptor file exist for this repo - if (FileUtil.exists(mondoDescriptorFile)) { - let mondoDescriptor = require(mondoDescriptorFile); - let rootRepoPath = Path.resolve(manifestPath, (mondoDescriptor.root === true ? '.' : mondoDescriptor.root) || '.'); + if (mondoDescriptorFile.exists()) { + let mondoDescriptor = mondoDescriptorFile.load(); + let rootRepoPath = manifestPath.join((mondoDescriptor.root === true ? '.' : mondoDescriptor.root) || '.'); // Descriptor has a path pointing to the root repo - if (rootRepoPath !== manifestPath) { - let rootRepoManifestFile = Path.resolve(rootRepoPath, constants.manifest); - root = new Repo({path: rootRepoManifestFile}); + if (!rootRepoPath.equals(manifestPath)) { + let rootRepoManifestFile = rootRepoPath.join(me.config.get('manifest')); + root = new Repo({path: rootRepoManifestFile, config: me.config}); root._root = root; root.open(); - root.registerRepo(this.name, this); + root.registerRepo(me.name, me); } } - this._root = root || (root = this); + me._root = root || (root = me); } return root; @@ -293,13 +332,14 @@ class Repo { * @property {Repo[]} uses */ get uses() { - let uses = this._uses; + let me = this; + let uses = me._uses; if (!uses) { - let manifest = this.manifest; + let manifest = me.manifest; if (manifest) { - let mondo = this.mondo; + let mondo = me.mondo; uses = new Collection(); if (mondo) { @@ -307,11 +347,11 @@ class Repo { const names = Object.keys(manifestUses); names.forEach(name => { - const repo = this.resolveRepo(name, manifestUses[name]); + const repo = me.resolveRepo(name, manifestUses[name]); uses.add(repo); }); - this._uses = uses; + me._uses = uses; } } else { throw new Error('Unable to get uses. Mondo Manifest is not set for this repo'); @@ -326,45 +366,49 @@ class Repo { } get allUses() { - if (!this._allUses) { - const graph = new Graph(this); - this._allUses = graph.depends; + let me = this; + if (!me._allUses) { + const graph = new Graph(me); + me._allUses = graph.depends; } - return this._allUses; + return me._allUses; } /** * @property {Package[]} visiblePackages */ get visiblePackages() { - let visiblePackages = this._visiblePackages; + let me = this; + let visiblePackages = me._visiblePackages; if (!visiblePackages) { - let manifest = this.manifest; + let manifest = me.manifest; if (manifest) { - visiblePackages = this.packages.clone(); + visiblePackages = me.packages.clone(); - this.uses.forEach(repo => { + me.uses.forEach(repo => { visiblePackages.addAll(repo.packages); }); - this._visiblePackages = visiblePackages; + me._visiblePackages = visiblePackages; } else { - throw new Error(`Unable to get visible packages from a repo without path information. Configure 'path' for '${this.name}'`); + throw new Error(`Unable to get visible packages from a repo without path information. Configure 'path' for '${me.name}'`); } } return visiblePackages; } + //---------------------- Instance Methods ----------------------// + /** * @returns {boolean} * @private */ exists() { - return FileUtil.exists(this._manifestFile); + return this._manifestFile.exists(); } /** @@ -373,7 +417,8 @@ class Repo { * @private */ resolveRepo(name, source) { - let root = this.root; + let me = this; + let root = me.root; let registry = root._registry; let repo = registry[name]; @@ -395,10 +440,15 @@ class Repo { throw new Error(`Attempt to register a non-root Repo without source information. Configure 'source' for '${repo.name}'`); } } else { - let installDir = this.installDir; - let repoPath = Path.resolve(installDir, name, constants.manifest); - let config = {name, source: source, path: repoPath, _root: root}; - repo = new Repo(config); + let installDir = me.installDir; + let repoPath = installDir.join(name).join(me.config.get('manifest')); + repo = new Repo({ + name, + source: source, + path: repoPath, + _root: root, + config: me.config + }); if (repo.exists()) { repo.open(); } @@ -419,7 +469,7 @@ class Repo { if (registry[name]) { throw new Error(`Repo ${name} already registered`); } - + registry[name] = repo; } @@ -427,12 +477,15 @@ class Repo { * @private */ open() { - if (!this._manifest) { - if (this.exists()) { - this._manifest = require(this._manifestFile); - this.name = (this._manifest.mondo && this._manifest.mondo.name) || this._manifest.name; + let me = this; + me.log.debug(`Opening repo from ${me._manifestFile}`); + if (!me._manifest) { + me._manifestFile = me._manifestPath.join(me.config.get('manifest')); + if (me.exists()) { + me._manifest = me._manifestFile.load(); + me.name = (me._manifest.mondo && me._manifest.mondo.name) || me._manifest.name; } else { - throw new Error(`Unable to find Repo manifest at '${this._manifestFile}`); + throw new Error(`Unable to find Repo manifest at '${me._manifestFile}`); } } } diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..8d99750 --- /dev/null +++ b/src/util.js @@ -0,0 +1,19 @@ +const Util = { + /** + * Receives an array of functions that in turn return a promise + * Example: + * + * promiseSerial([ + * () => new Promise((resolve, reject) => { console.log('Hello'); resolve(); }), + * () => new Promise((resolve, reject) => { console.log('How are you?'); resolve(); }) + * ]).then(() => { + * console.log("Goodbye!"); + * }); + */ + promiseSerial : (funcs) => + funcs.reduce((promise, func) => + promise.then(result => func().then(Array.prototype.concat.bind(result))), + Promise.resolve([])) +}; + +module.exports = Util; diff --git a/src/utils/FileUtil.js b/src/utils/FileUtil.js deleted file mode 100644 index 51a1389..0000000 --- a/src/utils/FileUtil.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -const Path = require('path'); -const fs = require("fs"); - -const mkdirp = require('mkdirp'); -const cwd = process.cwd(); - -class FileUtil { - - static absolute(filepath) { - if (!Path.isAbsolute(filepath)) { - return Path.resolve(cwd, filepath); - } - - return filepath; - } - - static findClosestPackage(dir) { - let testDir = Path.resolve(dir, 'package.json'); - if (FileUtil.exists(testDir)) { - return testDir; - } else { - let parent = Path.resolve(dir, '..'); - if (dir !== parent) { - return FileUtil.findClosestPackage(parent); - } - } - } - - /** - * @param file - * @returns {boolean} - */ - static exists(file) { - try { - fs.accessSync(file, fs.F_OK); - return true; - } catch (e) { - return false; - } - } - - static isFile(path) { - if (FileUtil.exists(path)) { - return fs.lstatSync(path).isFile(); - } - } - - static mkdirp (path) { - mkdirp.sync(path); - } - - static getFileContents (path) { - return fs.readFileSync(path, 'UTF-8'); - } - - static writeFile (path, contents) { - const dir = Path.dirname(path); - if (!fs.existsSync(dir)) { - FileUtil.mkdirp(dir); - } - - return fs.writeFileSync(path, contents); - } -} - -module.exports = FileUtil; diff --git a/src/utils/Logger.js b/src/utils/Logger.js deleted file mode 100644 index 0b4f532..0000000 --- a/src/utils/Logger.js +++ /dev/null @@ -1,61 +0,0 @@ -const levels = ['error', 'warn', 'info', 'debug']; - -class Logger { - constructor () { - this.muted = 0; - this.setThreshold('info'); - } - - log (...args) { - if (!this.muted) { - console.log(...args); - } - } - - error(...args) { - if (!this.muted) { - console.error(...args); - } - } - - warn(...args) { - if (!this.muted) { - console.warn(...args); - } - } - - info(...args) { - if (!this.muted && this.info.enabled) { - console.info(...args); - } - } - - debug(...args) { - if (!this.muted && this.debug.enabled) { - console.log(...args); - } - } - - setThreshold (threshold = 'info') { - let enabled = true; - - for (let level of levels) { - this[level].enabled = enabled; - if (level === threshold) { - enabled = false; - } - } - } - - mute () { - this.muted++; - } - - unmute () { - if (this.muted) { - this.muted--; - } - } -} - -module.exports = new Logger(); diff --git a/src/vcs/Git.js b/src/vcs/Git.js deleted file mode 100644 index dd924ce..0000000 --- a/src/vcs/Git.js +++ /dev/null @@ -1,56 +0,0 @@ -const exec = require('child_process').execSync; -const chalk = require('chalk'); -const SimpleGit = require('simple-git'); - -const Logger = require('../utils/Logger'); -const VCSBase = require('./Base'); -const constants = require('../constants'); - -class Git extends VCSBase { - available() { - try { - exec('git --version'); - return true; - } catch (e) { - return false; - } - } - - clone (repoPath, path, branch = "master") { - let me = this; - return new Promise((resolve, reject) => { - const fork = me.forks[repoPath]; - const originalRepoPath = repoPath; - - if (fork) { - repoPath = fork; - Logger.info(`Fork Detected installing from '${chalk.yellow(repoPath)}#${chalk.magenta(branch)}' into '${path}'`); - } - - // SimpleGit().clone(`git@github.com:${repoPath}.git`, path, ['-b', branch, '--depth', '1', '--no-single-branch'], (err) => { - SimpleGit().clone(`git@github.com:${repoPath}.git`, path, ['-b', branch], (err) => { - if (err) { - reject(err); - } else { - if (fork) { - SimpleGit(path).addRemote(constants.forkedRepoName, `git@github.com:${originalRepoPath}.git`, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } else { - resolve(); - } - } - }); - }); - } - - process (repository, path, branch) { - return this.clone(repository, path, branch); - } -} - -module.exports = Git; diff --git a/src/vcs/Base.js b/src/vcs/base.js similarity index 56% rename from src/vcs/Base.js rename to src/vcs/base.js index f460d5d..63eab9e 100644 --- a/src/vcs/Base.js +++ b/src/vcs/base.js @@ -1,5 +1,10 @@ +const Config = require('../config'); + class VCSBase { constructor (opts) { + if (!opts.hasOwnProperty('config')) { + opts.config = new Config(); + } Object.assign(this, opts); } @@ -8,7 +13,7 @@ class VCSBase { } available() { - return true; + throw new Error("Not yet implemented"); } } diff --git a/src/vcs/git.js b/src/vcs/git.js new file mode 100644 index 0000000..bd55a85 --- /dev/null +++ b/src/vcs/git.js @@ -0,0 +1,80 @@ +const exec = require('child_process').execSync; +const chalk = require('chalk'); +const SimpleGit = require('simple-git'); +const File = require('phylo'); + +const VCSBase = require('./base'); + +class Git extends VCSBase { + available() { + try { + exec('git --version'); + return true; + } catch (e) { + return false; + } + } + + clone (repoPath, path, branch = "master") { + let me = this; + return new Promise((resolve, reject) => { + const fork = me.forks[repoPath]; + const originalRepoPath = repoPath; + + if (!fork) { + me.log.error(`A fork of project '${repoPath}' could not be found.`); + return reject(`Use ${chalk.yellow(`mondo fork add ${repoPath} `)} to add one.`); + } + + repoPath = fork; + me.log.info(`Cloning '${chalk.yellow(originalRepoPath)}#${chalk.magenta(branch)}' into '${chalk.magenta(path.relativePath(File.cwd()))}'`); + let simpleGit = SimpleGit(); + if (me.debug) { + simpleGit.outputHandler((cmd, stdout, stderr) => { + stdout.on('data', (data) => { + me.log.indent().debug(`${data.toString()}`).outdent(); + }); + stderr.on('data', (data) => { + me.log.indent().debug(`${data.toString()}`).outdent(); + }); + }); + } + // SimpleGit().clone(`git@github.com:${repoPath}.git`, path, ['-b', branch, '--depth', '1', '--no-single-branch'], (err) => { + simpleGit.clone(`git@github.com:${repoPath}.git`, path.path, ['-b', branch], (err) => { + if (err) { + reject(err); + } else { + if (fork) { + simpleGit = SimpleGit(path.path); + if (me.debug) { + simpleGit.outputHandler((cmd, stdout, stderr) => { + stdout.on('data', (data) => { + me.log.indent().debug(`${data.toString()}`).outdent(); + }); + stderr.on('data', (data) => { + me.log.indent().debug(`${data.toString()}`).outdent(); + }); + }); + } + me.log.debug(`Fork Detected installing from '${chalk.yellow(repoPath)}#${chalk.magenta(branch)}' into '${path}'`); + simpleGit.addRemote(me.config.get('forkedRepoName'), `git@github.com:${originalRepoPath}.git`, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } else { + resolve(); + } + } + }); + }); + } + + process (repository, path, branch) { + return this.clone(repository, path, branch); + } +} + +module.exports = Git; diff --git a/src/VCS.js b/src/vcs/index.js similarity index 69% rename from src/VCS.js rename to src/vcs/index.js index 1bfafa1..0deefb5 100644 --- a/src/VCS.js +++ b/src/vcs/index.js @@ -1,4 +1,4 @@ -const Git = require('./vcs/Git'); +const git = require('./git'); class VCS { static registerVCS (name, vcs) { @@ -9,6 +9,6 @@ class VCS { } VCS._systems = {}; -VCS.registerVCS('git', Git); +VCS.registerVCS('git', git); -module.exports = VCS; \ No newline at end of file +module.exports = VCS;