diff --git a/package.json b/package.json index 0a797ec0..3594007f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "build:legacy": "BABEL_ENV=legacy babel src -d dist/legacy", "build:modern": "BABEL_ENV=modern babel src -d dist/modern", "build": "yarn run clean && yarn build:legacy && yarn build:modern", + "dev": "yarn run clean && yarn build:modern --watch", "prepublish": "yarn build", "precommit": "lint-staged" }, @@ -32,14 +33,15 @@ "babel-preset-env": "^1.6.0", "babel-preset-flow": "^6.23.0", "fixturez": "^1.1.0", - "flow-bin": "^0.63.1", + "flow-bin": "^0.66.0", "husky": "^0.14.3", "jest": "^20.0.4", "jest-expect-contain-deep": "^1.0.1", "lint-staged": "^4.1.3", "mode-to-permissions": "^0.0.2", "path-exists": "^3.0.0", - "prettier": "^1.6.1" + "prettier": "^1.6.1", + "sinon": "^4.1.4" }, "dependencies": { "array-includes": "^3.0.3", @@ -51,7 +53,7 @@ "detect-indent": "^5.0.0", "find-up": "^2.1.0", "globby": "^6.1.0", - "inquirer": "3.3.0", + "inquirer": "^4.0.2", "is-glob": "^4.0.0", "make-dir": "^1.0.0", "meow": "^4.0.0", diff --git a/src/DependencyGraph.js b/src/DependencyGraph.js new file mode 100644 index 00000000..8594d603 --- /dev/null +++ b/src/DependencyGraph.js @@ -0,0 +1,95 @@ +// @flow +import Project from './Project'; +import Workspace from './Workspace'; +import semver from 'semver'; +import * as logger from './utils/logger'; +import * as messages from './utils/messages'; + +export type SourceDepGraph = Map< + Workspace, + { + dependencies: Set<Workspace>, + dependents: Set<Workspace> + } +>; + +export default class DependencyGraph { + _workspaces: Map< + Workspace, + { + dependencies: Set<Workspace>, + dependents: Set<Workspace> + } + >; + _workspacesByName: { [key: string]: Workspace }; + _isValid: boolean; + + constructor(project: Project, workspaces: Array<Workspace>) { + this._workspaces = new Map(); + this._isValid = true; + this._workspacesByName = {}; + + for (let workspace of workspaces) { + this._workspacesByName[workspace.pkg.config.getName()] = workspace; + this._workspaces.set(workspace, { + dependencies: new Set(), + dependents: new Set() + }); + } + + for (let workspace of workspaces) { + let allDependencies = workspace.pkg.getAllDependencies(); + + for (let [depName, depVersion] of allDependencies) { + let match = this._workspacesByName[depName]; + if (!match) continue; + + let actual = depVersion; + let expected = match.pkg.config.getVersion(); + + if (!semver.satisfies(expected, depVersion)) { + this._isValid = false; + logger.error( + messages.packageMustDependOnCurrentVersion( + workspace.pkg.config.getName(), + depName, + expected, + depVersion + ) + ); + continue; + } + + let wNode = this._workspaces.get(workspace); + let mNode = this._workspaces.get(match); + + if (wNode) wNode.dependencies.add(match); + if (mNode) mNode.dependents.add(workspace); + } + } + } + + getDepsByWorkspace(workspace: Workspace) { + return this._workspaces.get(workspace); + } + + getWorkspaceByName(pkgName: string) { + return this._workspacesByName[pkgName]; + } + + getDepsByName(pkgName: string) { + return this.getDepsByWorkspace(this.getWorkspaceByName(pkgName)); + } + + has(pkgName: string) { + return !!this.getWorkspaceByName(pkgName); + } + + isValid() { + return this._isValid; + } + + entries() { + return this._workspaces.entries(); + } +} diff --git a/src/Project.js b/src/Project.js index 4ad53d84..21d5398f 100644 --- a/src/Project.js +++ b/src/Project.js @@ -13,9 +13,19 @@ import { BoltError } from './utils/errors'; import * as globs from './utils/globs'; import taskGraphRunner from 'task-graph-runner'; import minimatch from 'minimatch'; +import Repository from './Repository'; +import DependencyGraph from './DependencyGraph'; export type Task = (workspace: Workspace) => Promise<mixed>; +export type DepGraph = Map< + Workspace, + { + dependencies: Set<Workspace>, + dependents: Set<Workspace> + } +>; + export default class Project { pkg: Package; @@ -24,9 +34,11 @@ export default class Project { } static async init(cwd: string) { - let filePath = await Config.getProjectConfig(cwd); - if (!filePath) + let realPath = await fs.realpath(cwd); + let filePath = await Config.getProjectConfig(realPath); + if (!filePath) { throw new BoltError(`Unable to find root of project in ${cwd}`); + } let pkg = await Package.init(filePath); return new Project(pkg); } @@ -57,110 +69,18 @@ export default class Project { return workspaces; } - async getDependencyGraph(workspaces: Array<Workspace>) { - let graph: Map< - string, - { pkg: Package, dependencies: Array<string> } - > = new Map(); - let packages = [this.pkg]; - let packagesByName = { [this.pkg.config.getName()]: this.pkg }; - let valid = true; - - for (let workspace of workspaces) { - packages.push(workspace.pkg); - packagesByName[workspace.pkg.config.getName()] = workspace.pkg; - } - - for (let pkg of packages) { - let name = pkg.config.getName(); - let dependencies = []; - let allDependencies = pkg.getAllDependencies(); - - for (let [depName, depVersion] of allDependencies) { - let match = packagesByName[depName]; - if (!match) continue; - - let actual = depVersion; - let expected = match.config.getVersion(); - - // Workspace dependencies only need to semver satisfy, not '===' - if (!semver.satisfies(expected, depVersion)) { - valid = false; - logger.error( - messages.packageMustDependOnCurrentVersion( - name, - depName, - expected, - depVersion - ) - ); - continue; - } - - dependencies.push(depName); - } - - graph.set(name, { pkg, dependencies }); - } - - return { graph, valid }; - } - - async getDependentsGraph(workspaces: Array<Workspace>) { - let graph = new Map(); - let { valid, graph: dependencyGraph } = await this.getDependencyGraph( - workspaces - ); - - let dependentsLookup: { - [string]: { pkg: Package, dependents: Array<string> } - } = {}; - - workspaces.forEach(workspace => { - dependentsLookup[workspace.pkg.config.getName()] = { - pkg: workspace.pkg, - dependents: [] - }; - }); - - workspaces.forEach(workspace => { - let dependent = workspace.pkg.config.getName(); - let valFromDependencyGraph = dependencyGraph.get(dependent) || {}; - let dependencies = valFromDependencyGraph.dependencies || []; - - dependencies.forEach(dependency => { - dependentsLookup[dependency].dependents.push(dependent); - }); - }); - - // can't use Object.entries here as the flow type for it is Array<[string, mixed]>; - Object.keys(dependentsLookup).forEach(key => { - graph.set(key, dependentsLookup[key]); - }); - - return { valid, graph }; - } - async runWorkspaceTasks(workspaces: Array<Workspace>, task: Task) { - let { graph: dependentsGraph, valid } = await this.getDependencyGraph( - workspaces - ); - + let depGraph = new DependencyGraph(this, workspaces); let graph = new Map(); - for (let [pkgName, pkgInfo] of dependentsGraph) { - graph.set(pkgName, pkgInfo.dependencies); + for (let [workspace, { dependencies }] of depGraph.entries()) { + graph.set(workspace, Array.from(dependencies)); } let { safe } = await taskGraphRunner({ graph, force: true, - task: async workspaceName => { - let workspace = this.getWorkspaceByName(workspaces, workspaceName); - if (workspace) { - return task(workspace); - } - } + task }); if (!safe) { diff --git a/src/Workspace.js b/src/Workspace.js index 1665a7bb..30fe5013 100644 --- a/src/Workspace.js +++ b/src/Workspace.js @@ -11,4 +11,8 @@ export default class Workspace { static async init(pkg: Package) { return new Workspace(pkg); } + + getName() { + return this.pkg.config.getName(); + } } diff --git a/src/__tests__/Project.test.js b/src/__tests__/Project.test.js index 13cc4034..60066ffd 100644 --- a/src/__tests__/Project.test.js +++ b/src/__tests__/Project.test.js @@ -1,8 +1,9 @@ // @flow import path from 'path'; -import Project from '../Project'; +import Project, { type DepGraph } from '../Project'; import Package from '../Package'; import Workspace from '../Workspace'; +import DependencyGraph from '../DependencyGraph'; import * as logger from '../utils/logger'; import fixtures from 'fixturez'; @@ -15,9 +16,24 @@ function assertDependencies(graph, pkg, dependencies) { expect(val && val.dependencies).toEqual(dependencies); } -function assertDependents(graph, pkg, dependents) { - let val = graph.get(pkg); - expect(val && val.dependents).toEqual(dependents); +type ExpectedDepGraph = { + [pkgName: string]: { + dependencies: Array<string>, + dependents: Array<string> + } +}; + +function assertDepGraph(graph: DependencyGraph, expected: ExpectedDepGraph) { + let actual: ExpectedDepGraph = {}; + + for (let [workspace, { dependencies, dependents }] of graph.entries()) { + actual[workspace.getName()] = { + dependencies: Array.from(dependencies).map(ws => ws.getName()), + dependents: Array.from(dependents).map(ws => ws.getName()) + }; + } + + expect(actual).toEqual(expected); } // Asserts that a set of workspaces contains all (and only) the expected ones @@ -31,7 +47,7 @@ function assertWorkspaces(workspaces, expectedNames) { } describe('Project', () => { - let project; + let project: Project; describe('A simple project', () => { beforeEach(async () => { @@ -74,40 +90,16 @@ describe('Project', () => { expect(workspaces[0]).toBeInstanceOf(Workspace); }); - it('should be able to getDependencyGraph', async () => { + it('should be able to getDepGraph', async () => { let workspaces = await project.getWorkspaces(); - let { valid, graph } = await project.getDependencyGraph(workspaces); - let expectedDependencies = { - 'fixture-project-nested-workspaces': [], - foo: ['bar'], - bar: [], - baz: ['bar'] - }; - - expect(valid).toEqual(true); - expect(graph).toBeInstanceOf(Map); - expect(graph.size).toBe(4); - - Object.entries(expectedDependencies).forEach(([pkg, dependencies]) => { - assertDependencies(graph, pkg, dependencies); - }); - }); + let graph = new DependencyGraph(project, workspaces); - it('should be able to getDependentsGraph', async () => { - let workspaces = await project.getWorkspaces(); - let { valid, graph } = await project.getDependentsGraph(workspaces); - let expectedDependents = { - bar: ['foo', 'baz'], - foo: [], - baz: [] - }; - - expect(valid).toEqual(true); - expect(graph).toBeInstanceOf(Map); - expect(graph.size).toBe(Object.keys(expectedDependents).length); - - Object.entries(expectedDependents).forEach(([pkg, dependents]) => { - assertDependents(graph, pkg, dependents); + expect(graph.isValid()).toEqual(true); + + assertDepGraph(graph, { + foo: { dependents: [], dependencies: ['bar'] }, + bar: { dependents: ['foo', 'baz'], dependencies: [] }, + baz: { dependents: [], dependencies: ['bar'] } }); }); }); @@ -124,47 +116,29 @@ describe('Project', () => { expect(workspaces[0]).toBeInstanceOf(Workspace); }); - it('should be able to getDependencyGraph', async () => { + it('should be able to getDepGraph', async () => { let workspaces = await project.getWorkspaces(); - let { valid, graph } = await project.getDependencyGraph(workspaces); - let expectedDependencies = { - 'nested-workspaces-transitive-dependents': [], - 'pkg-a': [], - 'workspace-a': ['pkg-a'], - 'pkg-b': ['pkg-a'], - 'pkg-c': ['pkg-b'] - }; - - expect(valid).toEqual(true); - expect(graph).toBeInstanceOf(Map); - expect(graph.size).toBe(Object.keys(expectedDependencies).length); - - let assertDependencies = (pkg, deps) => { - let val = graph.get(pkg); - expect(val && val.dependencies).toEqual(deps); - }; - - Object.entries(expectedDependencies).forEach(([pkg, dependencies]) => { - assertDependencies(pkg, dependencies); + let graph = new DependencyGraph(project, workspaces); + + expect(graph.isValid()).toEqual(true); + assertDepGraph(graph, { + 'pkg-a': { dependents: ['workspace-a', 'pkg-b'], dependencies: [] }, + 'workspace-a': { dependents: [], dependencies: ['pkg-a'] }, + 'pkg-b': { dependents: ['pkg-c'], dependencies: ['pkg-a'] }, + 'pkg-c': { dependents: [], dependencies: ['pkg-b'] } }); }); - it('should be able to getDependentsGraph', async () => { + it('should be able to getDepGraph', async () => { let workspaces = await project.getWorkspaces(); - let { valid, graph } = await project.getDependentsGraph(workspaces); - let expectedDependents = { - 'pkg-a': ['workspace-a', 'pkg-b'], - 'workspace-a': [], - 'pkg-b': ['pkg-c'], - 'pkg-c': [] - }; - - expect(valid).toEqual(true); - expect(graph).toBeInstanceOf(Map); - expect(graph.size).toBe(Object.keys(expectedDependents).length); - - Object.entries(expectedDependents).forEach(([pkg, dependents]) => { - assertDependents(graph, pkg, dependents); + let graph = new DependencyGraph(project, workspaces); + + expect(graph.isValid()).toEqual(true); + assertDepGraph(graph, { + 'pkg-a': { dependents: ['workspace-a', 'pkg-b'], dependencies: [] }, + 'pkg-b': { dependents: ['pkg-c'], dependencies: ['pkg-a'] }, + 'pkg-c': { dependents: [], dependencies: ['pkg-b'] }, + 'workspace-a': { dependents: [], dependencies: ['pkg-a'] } }); }); }); diff --git a/src/commands/__tests__/remove.test.js b/src/commands/__tests__/remove.test.js index 8033fa1e..e50256bb 100644 --- a/src/commands/__tests__/remove.test.js +++ b/src/commands/__tests__/remove.test.js @@ -12,14 +12,17 @@ jest.mock('../../utils/logger'); jest.mock('../../utils/yarn'); describe('bolt remove', () => { - test('removing a project dependency only used by the project', async () => { - let tempDir = f.copy('package-with-external-deps-installed'); + test.only( + 'removing a project dependency only used by the project', + async () => { + let tempDir = f.copy('package-with-external-deps-installed'); - await remove(toRemoveOptions(['project-only-dep'], { cwd: tempDir })); + await remove(toRemoveOptions(['project-only-dep'], { cwd: tempDir })); - expect(yarn.remove).toHaveBeenCalledTimes(1); - expect(yarn.remove).toHaveBeenCalledWith(['project-only-dep'], tempDir); - }); + expect(yarn.remove).toHaveBeenCalledTimes(1); + expect(yarn.remove).toHaveBeenCalledWith(['project-only-dep'], tempDir); + } + ); test('removing a workspace dependency', async () => { let tempDir = f.copy('package-with-external-deps-installed'); diff --git a/src/commands/__tests__/version.test.js b/src/commands/__tests__/version.test.js index 37a96798..4e0e76fd 100644 --- a/src/commands/__tests__/version.test.js +++ b/src/commands/__tests__/version.test.js @@ -1,4 +1,56 @@ -// @flow -import { version, toVersionOptions } from '../version'; - -test('bolt version'); +// // @flow +// import { version, toVersionOptions } from '../version'; +// import { copyFixtureIntoTempDir } from 'jest-fixtures'; +// import * as git from '../../utils/git'; +// import * as fs from '../../utils/fs'; +// import { BoltError } from '../../utils/errors'; +// import * as path from 'path'; +// import Project from '../../Project'; +// import * as semver from 'semver'; +// +// describe('bolt version', () => { +// test('dirty tree', async () => { +// let cwd = await copyFixtureIntoTempDir(__dirname, 'simple-repo'); +// let opts = toVersionOptions([], { cwd }); +// +// // unstaged +// await git.initRepository({ cwd }); +// await expect(version(opts)).rejects.toBeInstanceOf(BoltError); +// +// // staged +// await git.addAll({ cwd }); +// await expect(version(opts)).rejects.toBeInstanceOf(BoltError); +// +// // unstaged on top of commit +// await git.commit('init', { cwd }); +// await fs.writeFile(path.join(cwd, 'foo'), ''); +// await expect(version(opts)).rejects.toBeInstanceOf(BoltError); +// }); +// +// test('clean tree, no prev version commits', async () => { +// let cwd = await copyFixtureIntoTempDir(__dirname, 'simple-repo'); +// +// let opts = toVersionOptions([], { cwd }); +// let project = await Project.init(cwd); +// let workspaces = await project.getWorkspaces(); +// +// await git.initRepository({ cwd }); +// await git.addAll({ cwd }); +// await git.commit('init', { cwd }); +// +// await Promise.all( +// workspaces.slice(0, 1).map(async workspace => { +// let json = workspace.pkg.config.getConfig(); +// await workspace.pkg.config.write({ +// ...json, +// version: semver.inc(json.version, 'major') +// }); +// }) +// ); +// +// await git.addAll({ cwd }); +// await git.commit('bump', { cwd }); +// +// await version(opts); +// }); +// }); diff --git a/src/commands/add.js b/src/commands/add.js index 41703e4b..e4d0b6ff 100644 --- a/src/commands/add.js +++ b/src/commands/add.js @@ -4,14 +4,14 @@ import Package from '../Package'; import * as options from '../utils/options'; import * as logger from '../utils/logger'; import addDependenciesToPackage from '../utils/addDependenciesToPackages'; -import type { Dependency, configDependencyType } from '../types'; +import type { Dependency, ConfigDependencyType } from '../types'; import { DEPENDENCY_TYPE_FLAGS_MAP } from '../constants'; import type { ProjectAddOptions } from './project/add'; export type AddOptions = { cwd?: string, deps: Array<Dependency>, - type: configDependencyType + type: ConfigDependencyType }; export function toAddOptions( diff --git a/src/commands/project/add.js b/src/commands/project/add.js index ee802b98..3159738c 100644 --- a/src/commands/project/add.js +++ b/src/commands/project/add.js @@ -3,14 +3,14 @@ import addDependenciesToPackage from '../../utils/addDependenciesToPackages'; import Project from '../../Project'; import * as options from '../../utils/options'; import * as logger from '../../utils/logger'; -import type { Dependency, configDependencyType } from '../../types'; +import type { Dependency, ConfigDependencyType } from '../../types'; import { DEPENDENCY_TYPE_FLAGS_MAP } from '../../constants'; import { add } from '../add'; export type ProjectAddOptions = { cwd?: string, deps: Array<Dependency>, - type: configDependencyType + type: ConfigDependencyType }; export function toProjectAddOptions( diff --git a/src/commands/version.js b/src/commands/version.js index ecc704a9..b251954b 100644 --- a/src/commands/version.js +++ b/src/commands/version.js @@ -1,16 +1,178 @@ // @flow +import Project, { type DepGraph } from '../Project'; +import Repository from '../Repository'; +import Workspace from '../Workspace'; +import DependencyGraph from '../DependencyGraph'; import * as options from '../utils/options'; +import * as changes from '../utils/changes'; +import * as git from '../utils/git'; +import * as prompt from '../utils/prompt'; +import * as versions from '../utils/versions'; +import * as constants from '../constants'; +import * as messages from '../utils/messages'; import { BoltError } from '../utils/errors'; -export type VersionOptions = {}; +export type VersionOptions = { + cwd?: string +}; export function toVersionOptions( args: options.Args, flags: options.Flags ): VersionOptions { - return {}; + return { + cwd: options.string(flags.cwd, 'cwd') + }; +} + +type IncrementTypeChoice = versions.IncrementType | 'skip'; +type VersionChoice = 'diff' | IncrementTypeChoice; + +type VersionChoices = Array< + prompt.Separator | { name: messages.Message, value: VersionChoice } +>; + +function getNextVersionOptions( + currentVersion: versions.Version +): VersionChoices { + let choices: VersionChoices = []; + + choices.push( + { + name: messages.skip(), + value: 'skip' + }, + { + name: messages.diff(), + value: 'diff' + }, + prompt.separator() + ); + + if (versions.getPrereleaseType(currentVersion)) { + choices.push({ + name: messages.prerelease( + versions.increment(currentVersion, 'prerelease') + ), + value: 'prerelease' + }); + } + + versions.VERSION_TYPES.forEach(versionType => { + if ( + versionType === 'prerelease' && + !versions.getPrereleaseType(currentVersion) + ) { + return; + } + + choices.push({ + name: versions.getIncrementMessage(currentVersion, versionType), + value: versionType + }); + }); + + choices.push(prompt.separator()); + + return choices; +} + +async function workspaceDiff(workspace, versionCommits, repo, tty: boolean) { + let versionCommit = versionCommits.get(workspace); + + if (!versionCommit) { + versionCommit = git.MAGIC_EMPTY_STATE_COMMIT; + } + + return await git.getDiffForPathSinceCommit( + workspace.pkg.dir, + versionCommit.hash, + { cwd: repo.dir, tty } + ); +} + +let prevIncrementTypeChoice: IncrementTypeChoice = 'patch'; + +async function getNextVersion(workspace, versionCommits, repo) { + let name = workspace.pkg.config.getName(); + let currentVersionStr = workspace.pkg.config.getVersion(); + let currentVersion = versions.toVersion(currentVersionStr); + let nextVersion = null; + + while (!nextVersion) { + let choice: VersionChoice = await prompt.list( + messages.selectVersion(name, currentVersionStr), + getNextVersionOptions(currentVersion), + { default: prevIncrementTypeChoice } + ); + + if (choice === 'diff') { + await workspaceDiff(workspace, versionCommits, repo, true); + } else if (choice === 'skip') { + prevIncrementTypeChoice = 'skip'; + break; + } else { + prevIncrementTypeChoice = choice; + nextVersion = versions.increment(currentVersion, choice); + } + } + + return nextVersion; } export async function version(opts: VersionOptions) { - throw new BoltError('Unimplemented command "version"'); + let cwd = opts.cwd || process.cwd(); + let project = await Project.init(cwd); + let repo = await Repository.init(project.pkg.dir); + let workspaces = await project.getWorkspaces(); + let graph = new DependencyGraph(project, workspaces); + + let status = await git.status({ cwd: repo.dir }); + + if (status.length) { + throw new BoltError( + 'Cannot run `bolt version` while you have a dirty tree:\n\n' + + status + .split('\n') + .map(line => ` ${line}`) + .join('\n') + ); + } + + let versionCommits = await changes.getWorkspaceVersionCommits( + repo, + workspaces + ); + + let changedWorkspaces: Array<Workspace> = []; + + for (let workspace of workspaces) { + let diff = await workspaceDiff(workspace, versionCommits, repo, false); + if (diff.length) changedWorkspaces.push(workspace); + } + + let newVersions: Map<Workspace, versions.IncrementType> = new Map(); + let dependentPackageQueue = []; + + for (let changedWorkspace of changedWorkspaces) { + let nextVersion = await getNextVersion( + changedWorkspace, + versionCommits, + repo + ); + + if (nextVersion) { + newVersions.set(changedWorkspace, nextVersion); + + let node = graph.getDepsByWorkspace(changedWorkspace); + if (!node) continue; + + for (let dependent of node.dependents) { + } + } + } + + for (let [workspace, nextVersion] of newVersions) { + console.log(workspace.pkg.config.getName(), nextVersion); + } } diff --git a/src/commands/workspace/add.js b/src/commands/workspace/add.js index 9c0b66eb..958150dd 100644 --- a/src/commands/workspace/add.js +++ b/src/commands/workspace/add.js @@ -5,14 +5,14 @@ import * as options from '../../utils/options'; import * as logger from '../../utils/logger'; import addDependenciesToPackage from '../../utils/addDependenciesToPackages'; import { BoltError } from '../../utils/errors'; -import type { Dependency, configDependencyType } from '../../types'; +import type { Dependency, ConfigDependencyType } from '../../types'; import { DEPENDENCY_TYPE_FLAGS_MAP } from '../../constants'; export type WorkspaceAddOptions = { cwd?: string, workspaceName: string, deps: Array<Dependency>, - type: configDependencyType + type: ConfigDependencyType }; export function toWorkspaceAddOptions( diff --git a/src/constants.js b/src/constants.js index 6b6b0aa8..e8adb6b0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -23,3 +23,13 @@ export const DEPENDENCY_TYPE_FLAGS_MAP = { }; export const BOLT_VERSION = boltPkg.version; + +export const SEMVER_TYPES = [ + 'patch', + 'minor', + 'major', + 'prerelease', + 'prepatch', + 'preminor', + 'premajor' +]; diff --git a/src/functions/getDependencyGraph.js b/src/functions/getDependencyGraph.js index 72acce1c..49109924 100644 --- a/src/functions/getDependencyGraph.js +++ b/src/functions/getDependencyGraph.js @@ -1,34 +1,34 @@ // @flow import Project from '../Project'; +import DependencyGraph from '../DependencyGraph'; import * as yarn from '../utils/yarn'; type Options = { cwd?: string }; -type DependencyGraph = Map<string, Array<string>>; +type DepGraph = Map<string, Array<string>>; export default async function getDependencyGraph( opts: Options = {} -): Promise<DependencyGraph> { +): Promise<DepGraph> { let cwd = opts.cwd || process.cwd(); - let project = await Project.init(cwd); + let project: Project = await Project.init(cwd); let workspaces = await project.getWorkspaces(); + let graph = new DependencyGraph(project, workspaces); - let { - graph: dependencyGraph, - valid: graphIsValid - } = await project.getDependencyGraph(workspaces); - - if (!graphIsValid) { + if (!graph.isValid()) { throw new Error('Dependency graph is not valid'); } let simplifiedDependencyGraph = new Map(); - dependencyGraph.forEach((pkgInfo, pkgName) => { - simplifiedDependencyGraph.set(pkgName, pkgInfo.dependencies); - }); + for (let [workspace, { dependencies }] of graph.entries()) { + simplifiedDependencyGraph.set( + workspace.getName(), + Array.from(dependencies).map(dep => dep.getName()) + ); + } return simplifiedDependencyGraph; } diff --git a/src/functions/getDependentsGraph.js b/src/functions/getDependentsGraph.js index 643a1155..4e388bc4 100644 --- a/src/functions/getDependentsGraph.js +++ b/src/functions/getDependentsGraph.js @@ -1,34 +1,34 @@ // @flow import Project from '../Project'; +import DependencyGraph from '../DependencyGraph'; import * as yarn from '../utils/yarn'; type Options = { cwd?: string }; -type DependentsGraph = Map<string, Array<string>>; +type DepGraph = Map<string, Array<string>>; export default async function getDependentsGraph( opts: Options = {} -): Promise<DependentsGraph> { +): Promise<DepGraph> { let cwd = opts.cwd || process.cwd(); - let project = await Project.init(cwd); + let project: Project = await Project.init(cwd); let workspaces = await project.getWorkspaces(); + let graph = new DependencyGraph(project, workspaces); - let { - graph: dependentsGraph, - valid: graphIsValid - } = await project.getDependentsGraph(workspaces); - - if (!graphIsValid) { + if (!graph.isValid()) { throw new Error('Dependents graph is not valid'); } let simplifiedDependentsGraph = new Map(); - dependentsGraph.forEach((pkgInfo, pkgName) => { - simplifiedDependentsGraph.set(pkgName, pkgInfo.dependents); - }); + for (let [workspace, { dependents }] of graph.entries()) { + simplifiedDependentsGraph.set( + workspace.getName(), + Array.from(dependents).map(dep => dep.getName()) + ); + } return simplifiedDependentsGraph; } diff --git a/src/functions/updatePackageVersions.js b/src/functions/updatePackageVersions.js index 30021f43..b34fb3b5 100644 --- a/src/functions/updatePackageVersions.js +++ b/src/functions/updatePackageVersions.js @@ -3,6 +3,7 @@ import semver from 'semver'; import Project from '../Project'; import Workspace from '../Workspace'; +import DependencyGraph from '../DependencyGraph'; import * as logger from '../utils/logger'; import * as messages from '../utils/messages'; import includes from 'array-includes'; @@ -41,14 +42,17 @@ export default async function updatePackageVersions( opts: Options = {} ): Promise<Array<string>> { let cwd = opts.cwd || process.cwd(); - let project = await Project.init(cwd); + let project: Project = await Project.init(cwd); let workspaces = await project.getWorkspaces(); - let { graph } = await project.getDependencyGraph(workspaces); + let graph = new DependencyGraph(project, workspaces); let editedPackages = new Set(); - let internalDeps = Object.keys(updatedPackages).filter(dep => graph.has(dep)); + let internalDeps = Object.keys(updatedPackages).filter(dep => + graph.getDepsByName(dep) + ); + let externalDeps = Object.keys(updatedPackages).filter( - dep => !graph.has(dep) + dep => !graph.getDepsByName(dep) ); if (externalDeps.length !== 0) { diff --git a/src/functions/updateWorkspaceDependencies.js b/src/functions/updateWorkspaceDependencies.js index b2882075..af1cd18b 100644 --- a/src/functions/updateWorkspaceDependencies.js +++ b/src/functions/updateWorkspaceDependencies.js @@ -1,5 +1,6 @@ // @flow import Project from '../Project'; +import DependencyGraph from '../DependencyGraph'; type VersionMap = { [x: string]: any @@ -20,9 +21,8 @@ export default async function updateWorkspaceDependencies( opts: Options = {} ) { let cwd = opts.cwd || process.cwd(); - let project = await Project.init(cwd); + let project: Project = await Project.init(cwd); let workspaces = await project.getWorkspaces(); - let { graph } = await project.getDependencyGraph(workspaces); let editedPackages = new Set(); // Note: all dependencyToUpgrade are external dependencies diff --git a/src/types.js b/src/types.js index 578208f4..c9f5a128 100644 --- a/src/types.js +++ b/src/types.js @@ -28,7 +28,7 @@ export type Dependency = { version?: string }; -export type configDependencyType = +export type ConfigDependencyType = | 'dependencies' | 'devDependencies' | 'peerDependencies' diff --git a/src/utils/__mocks__/processes.js b/src/utils/__mocks__/processes.js new file mode 100644 index 00000000..f3a83082 --- /dev/null +++ b/src/utils/__mocks__/processes.js @@ -0,0 +1,49 @@ +const actualProcesses = require.requireActual('../processes'); +const processes = jest.genMockFromModule('../processes'); +const { ChildProcessError } = actualProcesses; + +function isArrayShape(actual, expected) { + return !expected.find((value, index) => { + return value !== actual[index]; + }); +} + +function isObjectShape(actual, expected) { + return !Object.keys(expected).find(key => { + return expected[key] === actual[key]; + }); +} + +const mocks = []; + +processes.spawn = jest.fn((cmd, args, opts) => { + let found = mocks.find(mock => { + if (cmd !== mock.cmd) return false; + if (!isArrayShape(args, mock.args)) return false; + if (!isObjectShape(opts, mock.opts)) return false; + return true; + }); + + let res = { + code: (found && found.code) || 0, + stdout: (found && found.stdout) || '', + stderr: (found && found.stderr) || '' + }; + + if (res.code === 0) { + return Promise.resolve(res); + } else { + return Project.reject( + new ChildProcessError(res.code, res.stdout, res.stderr) + ); + } +}); + +processes.ChildProcessError = ChildProcessError; + +// __mock('echo', ['test'], {}, { code: 0, stdout: '', stderr: '' }); +processes.__mock = ({ cmd, args, opts }, { stdout, stderr, code }) => { + mocks.push({ cmd, args, opts, stdout, stderr, code }); +}; + +module.exports = processes; diff --git a/src/utils/__mocks__/spawn.js b/src/utils/__mocks__/spawn.js deleted file mode 100644 index 06a49c50..00000000 --- a/src/utils/__mocks__/spawn.js +++ /dev/null @@ -1,38 +0,0 @@ -const actualSpawn = require.requireActual('../spawn'); -const spawn = jest.genMockFromModule('../spawn'); - -function isArrayShape(actual, expected) { - return !expected.find((value, index) => { - return value !== actual[index]; - }); -} - -function isObjectShape(actual, expected) { - return !Object.keys(expected).find(key => { - return expected[key] === actual[key]; - }); -} - -const mocks = []; - -spawn.default = async function mockSpawn(cmd, args, opts) { - let found = mocks.find(mock => { - if (cmd !== mock.cmd) return false; - if (!isArrayShape(args, mock.args)) return false; - if (!isObjectShape(opts, mock.opts)) return false; - return true; - }); - - let result = { code: 0, stdout: '', stderr: '' }; - - if (!found) { - return Promise.resolve(); - } -}; - -// __mock('echo', ['test'], {}, { code: 0, stdout: '', stderr: '' }); -spawn.__mock = ({cmd, args, opts}, {stdout, stderr, code}) => { - mocks.push({cmd, args, opts, stdout, stderr, code}); -}; - -module.exports = spawn; diff --git a/src/utils/__tests__/changes.test.js b/src/utils/__tests__/changes.test.js index 86743cd8..a32d9594 100644 --- a/src/utils/__tests__/changes.test.js +++ b/src/utils/__tests__/changes.test.js @@ -41,8 +41,8 @@ describe('changes', () => { workspaces ); - expect(versionCommits1.get(fooWorkspace)).toBe(null); - expect(versionCommits1.get(barWorkspace)).toBe(null); + expect(versionCommits1.get(fooWorkspace)).toBe(undefined); + expect(versionCommits1.get(barWorkspace)).toBe(undefined); await updateConfig(fooConfig, { version: '2.0.0' }); await git.addAll({ cwd }); diff --git a/src/utils/__tests__/symlinkPackageDependencies.test.js b/src/utils/__tests__/symlinkPackageDependencies.test.js index 8740a844..af8e4169 100644 --- a/src/utils/__tests__/symlinkPackageDependencies.test.js +++ b/src/utils/__tests__/symlinkPackageDependencies.test.js @@ -42,6 +42,12 @@ describe('utils/symlinkPackageDependencies()', () => { let tempDir = f.copy('nested-workspaces-with-root-dependencies-installed'); project = await Project.init(tempDir); workspaces = await project.getWorkspaces(); + // We use the foo package as it has internal and external dependencies + let workspaceToSymlink = + project.getWorkspaceByName(workspaces, 'foo') || {}; + pkgToSymlink = workspaceToSymlink.pkg; + nodeModules = pkgToSymlink.nodeModules; + nodeModulesBin = pkgToSymlink.nodeModulesBin; }); /******************** @@ -49,15 +55,6 @@ describe('utils/symlinkPackageDependencies()', () => { ********************/ describe('linking packages', () => { - beforeEach(() => { - // We use the foo package as it has internal and external dependencies - let workspaceToSymlink = - project.getWorkspaceByName(workspaces, 'foo') || {}; - pkgToSymlink = workspaceToSymlink.pkg; - nodeModules = pkgToSymlink.nodeModules; - nodeModulesBin = pkgToSymlink.nodeModulesBin; - }); - it('should create node modules and node_modules/.bin if not existing', async () => { expect(await dirExists(pkgToSymlink.nodeModules)).toEqual(false); expect(await dirExists(pkgToSymlink.nodeModulesBin)).toEqual(false); @@ -141,13 +138,18 @@ describe('utils/symlinkPackageDependencies()', () => { }); }); - it('should symlink internal dependencies bin files (when declared using string)', async () => { - expect(await symlinkExists(nodeModulesBin, 'bar')).toEqual(false); + it.only( + 'should symlink internal dependencies bin files (when declared using string)', + async () => { + expect(await symlinkExists(nodeModulesBin, 'bar')).toEqual(false); - await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); + console.log(pkgToSymlink); - expect(await symlinkExists(nodeModulesBin, 'bar')).toEqual(true); - }); + await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); + + expect(await symlinkExists(nodeModulesBin, 'bar')).toEqual(true); + } + ); it('should symlink internal dependencies bin files (when declared using object)', async () => { expect(await symlinkExists(nodeModulesBin, 'baz-1')).toEqual(false); diff --git a/src/utils/__tests__/versions.test.js b/src/utils/__tests__/versions.test.js new file mode 100644 index 00000000..b859e7d3 --- /dev/null +++ b/src/utils/__tests__/versions.test.js @@ -0,0 +1,47 @@ +// @flow +import * as versions from '../versions'; + +const ONE_POINT_OH = versions.toVersion('1.0.0'); +const BETA_VERSION = versions.toVersion('2.0.0-beta.2'); +const ALPHA_VERSION = versions.toVersion('3.0.0-alpha.0'); + +describe('versions', () => { + test('.toVersion()', () => { + expect(versions.toVersion('1.0.0')).toBe('1.0.0'); + expect(() => versions.toVersion('nope')).toThrow(); + }); + + test('.getPrereleaseType()', () => { + expect(versions.getPrereleaseType(ONE_POINT_OH)).toBe(null); + expect(versions.getPrereleaseType(BETA_VERSION)).toBe('beta'); + expect(versions.getPrereleaseType(ALPHA_VERSION)).toBe('alpha'); + }); + + test('.increment()', () => { + expect(versions.increment(ONE_POINT_OH, 'patch')).toBe('1.0.1'); + expect(versions.increment(ONE_POINT_OH, 'minor')).toBe('1.1.0'); + expect(versions.increment(ONE_POINT_OH, 'major')).toBe('2.0.0'); + expect(versions.increment(ONE_POINT_OH, 'prepatch')).toBe('1.0.1-beta.0'); + expect(versions.increment(ONE_POINT_OH, 'preminor')).toBe('1.1.0-beta.0'); + expect(versions.increment(ONE_POINT_OH, 'premajor')).toBe('2.0.0-beta.0'); + expect(versions.increment(ONE_POINT_OH, 'prerelease')).toBe('1.0.1-beta.0'); + + expect(versions.increment(BETA_VERSION, 'patch')).toBe('2.0.0'); + expect(versions.increment(BETA_VERSION, 'minor')).toBe('2.0.0'); + expect(versions.increment(BETA_VERSION, 'major')).toBe('2.0.0'); + expect(versions.increment(BETA_VERSION, 'prepatch')).toBe('2.0.1-beta.0'); + expect(versions.increment(BETA_VERSION, 'preminor')).toBe('2.1.0-beta.0'); + expect(versions.increment(BETA_VERSION, 'premajor')).toBe('3.0.0-beta.0'); + expect(versions.increment(BETA_VERSION, 'prerelease')).toBe('2.0.0-beta.3'); + + expect(versions.increment(ALPHA_VERSION, 'patch')).toBe('3.0.0'); + expect(versions.increment(ALPHA_VERSION, 'minor')).toBe('3.0.0'); + expect(versions.increment(ALPHA_VERSION, 'major')).toBe('3.0.0'); + expect(versions.increment(ALPHA_VERSION, 'prepatch')).toBe('3.0.1-alpha.0'); + expect(versions.increment(ALPHA_VERSION, 'preminor')).toBe('3.1.0-alpha.0'); + expect(versions.increment(ALPHA_VERSION, 'premajor')).toBe('4.0.0-alpha.0'); + expect(versions.increment(ALPHA_VERSION, 'prerelease')).toBe( + '3.0.0-alpha.1' + ); + }); +}); diff --git a/src/utils/addDependenciesToPackages.js b/src/utils/addDependenciesToPackages.js index 0930d1fd..be9e093f 100644 --- a/src/utils/addDependenciesToPackages.js +++ b/src/utils/addDependenciesToPackages.js @@ -3,9 +3,10 @@ import semver from 'semver'; import Project from '../Project'; -import type Workspace from '../Workspace'; -import type Package from '../Package'; -import type { Dependency, configDependencyType } from '../types'; +import Workspace from '../Workspace'; +import Package from '../Package'; +import DependencyGraph from '../DependencyGraph'; +import type { Dependency, ConfigDependencyType } from '../types'; import * as messages from './messages'; import { BoltError } from './errors'; import * as logger from './logger'; @@ -16,16 +17,17 @@ export default async function addDependenciesToPackage( project: Project, pkg: Package, dependencies: Array<Dependency>, - type?: configDependencyType = 'dependencies' + type?: ConfigDependencyType = 'dependencies' ) { let workspaces = await project.getWorkspaces(); + let graph = new DependencyGraph(project, workspaces); + let projectDependencies = project.pkg.getAllDependencies(); let pkgDependencies = pkg.getAllDependencies(); - let { graph: depGraph } = await project.getDependencyGraph(workspaces); let dependencyNames = dependencies.map(dep => dep.name); - let externalDeps = dependencies.filter(dep => !depGraph.has(dep.name)); - let internalDeps = dependencies.filter(dep => depGraph.has(dep.name)); + let externalDeps = dependencies.filter(dep => !graph.has(dep.name)); + let internalDeps = dependencies.filter(dep => graph.has(dep.name)); let externalDepsToInstallForProject = externalDeps.filter( dep => !projectDependencies.has(dep.name) @@ -64,8 +66,8 @@ export default async function addDependenciesToPackage( } for (let dep of internalDeps) { - let dependencyPkg = (depGraph.get(dep.name) || {}).pkg; - let internalVersion = dependencyPkg.config.getVersion(); + let dependencyWorkspace = graph.getWorkspaceByName(dep.name); + let internalVersion = dependencyWorkspace.pkg.config.getVersion(); // If no version is requested, default to caret at the current version let requestedVersion = dep.version || `^${internalVersion}`; if (!semver.satisfies(internalVersion, requestedVersion)) { diff --git a/src/utils/changes.js b/src/utils/changes.js index 5adaa171..33a4def3 100644 --- a/src/utils/changes.js +++ b/src/utils/changes.js @@ -6,7 +6,7 @@ import * as git from '../utils/git'; async function getLastVersionCommitForWorkspace( repo: Repository, workspace: Workspace -) { +): Promise<git.Commit | null> { let cwd = repo.dir; let filePath = workspace.pkg.config.filePath; let commits = await git.getCommitsToFile(filePath, { cwd }); @@ -16,9 +16,7 @@ async function getLastVersionCommitForWorkspace( let parentCommit = await git.getCommitParent(commit.hash, { cwd }); if (!parentCommit) continue; - let before = await git.showFileAtCommit(filePath, parentCommit, { - cwd - }); + let before = await git.showFileAtCommit(filePath, parentCommit, { cwd }); let after = await git.showFileAtCommit(filePath, commit.hash, { cwd }); let jsonBefore = JSON.parse(before); @@ -36,12 +34,12 @@ async function getLastVersionCommitForWorkspace( export async function getWorkspaceVersionCommits( repo: Repository, workspaces: Array<Workspace> -) { +): Promise<Map<Workspace, git.Commit>> { let versionCommits = new Map(); for (let workspace of workspaces) { let commit = await getLastVersionCommitForWorkspace(repo, workspace); - versionCommits.set(workspace, commit); + if (commit) versionCommits.set(workspace, commit); } return versionCommits; diff --git a/src/utils/git.js b/src/utils/git.js index 1aef5c43..d9296da5 100644 --- a/src/utils/git.js +++ b/src/utils/git.js @@ -18,7 +18,7 @@ const gitCommandLimit = pLimit(1); const GIT_LOG_LINE_FORMAT_FLAG = '--pretty=format:%H %s'; const GIT_LOG_LINE_FORMAT_SPLITTER = /^([a-zA-Z0-9]+) (.*)/; -opaque type CommitHash = string; +export opaque type CommitHash = string; export type Commit = { hash: CommitHash, @@ -188,16 +188,22 @@ export async function showFileAtCommit( export const MAGIC_EMPTY_STATE_HASH: CommitHash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; +export const MAGIC_EMPTY_STATE_COMMIT: Commit = { + hash: MAGIC_EMPTY_STATE_HASH, + message: '(empty state)' +}; + export async function getDiffForPathSinceCommit( filePath: string, commitHash: CommitHash, - opts: { cwd: string } + opts: { cwd: string, tty?: boolean } ) { let gitPath = toGitPath(opts.cwd, filePath); let { stdout } = await git( ['diff', commitHash, '--color=always', '--', filePath], { - cwd: opts.cwd + cwd: opts.cwd, + tty: opts.tty } ); return stdout.trim(); diff --git a/src/utils/messages.js b/src/utils/messages.js index 6d6331f3..fac68af4 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -362,3 +362,48 @@ export function errorWorkspacesUpgrade(filterOpts: Array<string>): Message { export function noNeedToSymlinkInternalDependency(): Message { return `Internal packages are symlinked, there is no need update them`; } + +export function diff(): Message { + return 'Diff'; +} + +export function skip(): Message { + return 'Skip (no new version)'; +} + +export function patch(nextVersion: string): Message { + return `Patch (${nextVersion})`; +} + +export function minor(nextVersion: string): Message { + return `Minor (${nextVersion})`; +} + +export function major(nextVersion: string): Message { + return `Major (${nextVersion})`; +} + +export function prerelease(nextVersion: string): Message { + return `Prerelease (${nextVersion})`; +} + +export function prepatch(nextVersion: string): Message { + return `Prepatch (${nextVersion})`; +} + +export function preminor(nextVersion: string): Message { + return `Preminor (${nextVersion})`; +} + +export function premajor(nextVersion: string): Message { + return `Premajor (${nextVersion})`; +} + +export function selectVersion( + pkgName: string, + currentVersion: string +): Message { + return `Select a new version for ${normalPkg( + pkgName + )} (currently ${currentVersion}, depends on changed package(s) "foo", "bar", "baz", and 3 others)`; +} diff --git a/src/utils/processes.js b/src/utils/processes.js index 611e9a45..c713c4a8 100644 --- a/src/utils/processes.js +++ b/src/utils/processes.js @@ -46,7 +46,7 @@ export function spawn( cmd: string, args: Array<string>, opts: SpawnOptions = {} -) { +): Promise<{ code: number, stdout: string, stderr: string }> { return limit( () => new Promise((resolve, reject) => { diff --git a/src/utils/prompt.js b/src/utils/prompt.js new file mode 100644 index 00000000..37a59e02 --- /dev/null +++ b/src/utils/prompt.js @@ -0,0 +1,64 @@ +// @flow +import inquirer from 'inquirer'; +import * as messages from './messages'; + +const prompt = inquirer.createPromptModule(); +const KEY = 'value'; + +type Choice<Value> = { name: messages.Message, value: Value }; +type Choices<Value> = Array<Choice<Value> | Separator>; +export opaque type Separator = Object; + +export function separator(): Separator { + return new inquirer.Separator(); +} + +export async function list<Value: string>( + message: messages.Message, + choices: Choices<Value>, + opts: { default?: Value } = {} +): Promise<Value> { + let answers = await prompt([ + { + type: 'list', + name: KEY, + message, + choices, + default: opts.default + } + ]); + return answers[KEY]; +} + +export async function input(message: messages.Message): Promise<string> { + let answers = await prompt([ + { + type: 'input', + name: KEY, + message + } + ]); + return answers[KEY]; +} + +export async function password(message: messages.Message): Promise<string> { + let answers = await prompt([ + { + type: 'password', + name: KEY, + message + } + ]); + return answers[KEY]; +} + +export async function editor(message: messages.Message): Promise<string> { + let answers = await prompt([ + { + type: 'editor', + name: KEY, + message + } + ]); + return answers[KEY]; +} diff --git a/src/utils/symlinkPackageDependencies.js b/src/utils/symlinkPackageDependencies.js index 9c7c6e87..90786614 100644 --- a/src/utils/symlinkPackageDependencies.js +++ b/src/utils/symlinkPackageDependencies.js @@ -4,8 +4,9 @@ import pathIsInside from 'path-is-inside'; import includes from 'array-includes'; import Project from '../Project'; -import type Workspace from '../Workspace'; -import type Package from '../Package'; +import DependencyGraph from '../DependencyGraph'; +import Workspace from '../Workspace'; +import Package from '../Package'; import { BoltError } from './errors'; import * as fs from './fs'; import * as logger from './logger'; @@ -20,13 +21,10 @@ export default async function symlinkPackageDependencies( let projectDeps = project.pkg.getAllDependencies(); let pkgDependencies = project.pkg.getAllDependencies(); let workspaces = await project.getWorkspaces(); - let { - graph: dependencyGraph, - valid: dependencyGraphValid - } = await project.getDependencyGraph(workspaces); + let depGraph = new DependencyGraph(project, workspaces); let pkgName = pkg.config.getName(); // get all the dependencies that are internal workspaces in this project - let internalDeps = (dependencyGraph.get(pkgName) || {}).dependencies || []; + let internalDeps = (depGraph.getDepsByName(pkgName) || {}).dependencies || []; let directoriesToCreate = []; let symlinksToCreate = []; @@ -44,7 +42,7 @@ export default async function symlinkPackageDependencies( let versionInPkg = pkg.getDependencyVersionRange(depName); // If dependency is internal we can ignore it (we symlink below) - if (dependencyGraph.has(depName)) { + if (!!depGraph.getDepsByName(depName)) { continue; } @@ -91,14 +89,14 @@ export default async function symlinkPackageDependencies( **********************************************************************/ for (let dependency of internalDeps) { - let depWorkspace = dependencyGraph.get(dependency) || {}; - let src = depWorkspace.pkg.dir; - let dest = path.join(pkg.nodeModules, dependency); + let depWorkspace = depGraph.getDepsByWorkspace(dependency) || {}; + let src = dependency.pkg.dir; + let dest = path.join(pkg.nodeModules, dependency.pkg.config.getName()); symlinksToCreate.push({ src, dest, type: 'junction' }); } - if (!dependencyGraphValid || !valid) { + if (!depGraph.isValid() || !valid) { throw new BoltError('Cannot symlink invalid set of dependencies.'); } @@ -163,15 +161,9 @@ export default async function symlinkPackageDependencies( // TODO: Same as above, we should really be making sure we get all the transitive bins as well for (let dependency of internalDeps) { - let depWorkspace = dependencyGraph.get(dependency) || {}; - let depBinFiles = - depWorkspace.pkg && - depWorkspace.pkg.config && - depWorkspace.pkg.config.getBin(); - - if (!depBinFiles) { - continue; - } + let depWorkspace = depGraph.getDepsByWorkspace(dependency) || {}; + let depBinFiles = dependency.pkg.config.getBin(); + if (!depBinFiles) continue; if (!includes(dependencies, dependency)) { // dependency is not one we are supposed to symlink right now @@ -180,8 +172,11 @@ export default async function symlinkPackageDependencies( if (typeof depBinFiles === 'string') { // package may be scoped, name will only be the second part - let binName = dependency.split('/').pop(); - let src = path.join(depWorkspace.pkg.dir, depBinFiles); + let binName = dependency.pkg.config + .getName() + .split('/') + .pop(); + let src = path.join(dependency.pkg.dir, depBinFiles); let dest = path.join(pkg.nodeModulesBin, binName); symlinksToCreate.push({ src, dest, type: 'exec' }); @@ -189,7 +184,7 @@ export default async function symlinkPackageDependencies( } for (let [binName, binPath] of Object.entries(depBinFiles)) { - let src = path.join(depWorkspace.pkg.dir, String(binPath)); + let src = path.join(dependency.pkg.dir, String(binPath)); let dest = path.join(pkg.nodeModulesBin, binName); symlinksToCreate.push({ src, dest, type: 'exec' }); diff --git a/src/utils/upgradeDependenciesInPackages.js b/src/utils/upgradeDependenciesInPackages.js index 7fce82cf..659ed076 100644 --- a/src/utils/upgradeDependenciesInPackages.js +++ b/src/utils/upgradeDependenciesInPackages.js @@ -1,8 +1,9 @@ // @flow import Project from '../Project'; -import type Workspace from '../Workspace'; -import type Package from '../Package'; +import Workspace from '../Workspace'; +import Package from '../Package'; +import DependencyGraph from '../DependencyGraph'; import type { Dependency } from '../types'; import * as messages from './messages'; import { BoltError } from './errors'; @@ -19,10 +20,20 @@ export default async function upgradeDependenciesInPackage( ) { let workspaces = await project.getWorkspaces(); let pkgDependencies = pkg.getAllDependencies(); - let { graph: depGraph } = await project.getDependencyGraph(workspaces); + let depGraph = new DependencyGraph(project, workspaces); + + let workspaceDependencies = dependencies.map(dep => { + return project.getWorkspaceByName(workspaces, dep.name); + }); + + let externalDeps = dependencies.filter( + dep => !depGraph.getDepsByName(dep.name) + ); + + let internalDeps = dependencies.filter(dep => + depGraph.getDepsByName(dep.name) + ); - let externalDeps = dependencies.filter(dep => !depGraph.has(dep.name)); - let internalDeps = dependencies.filter(dep => depGraph.has(dep.name)); let projectDependencies = project.pkg.getAllDependencies(); if (project.pkg.isSamePackage(pkg)) { diff --git a/src/utils/validateProject.js b/src/utils/validateProject.js index 30b8786e..b7ea5b22 100644 --- a/src/utils/validateProject.js +++ b/src/utils/validateProject.js @@ -13,7 +13,7 @@ export default async function validateProject(project: Project) { let workspaces = await project.getWorkspaces(); let projectDependencies = project.pkg.getAllDependencies(); let projectConfig = project.pkg.config; - let { graph: depGraph } = await project.getDependencyGraph(workspaces); + // let { graph: depGraph } = await project.getDepGraph(workspaces); let projectIsValid = true; diff --git a/src/utils/versions.js b/src/utils/versions.js new file mode 100644 index 00000000..a7b982c7 --- /dev/null +++ b/src/utils/versions.js @@ -0,0 +1,62 @@ +// @flow +import * as semver from 'semver'; +import * as messages from './messages'; + +export opaque type Version = string; + +export type IncrementType = + | 'patch' + | 'minor' + | 'major' + | 'prepatch' + | 'preminor' + | 'premajor' + | 'prerelease'; + +export const VERSION_TYPES = [ + 'patch', + 'minor', + 'major', + 'prerelease', + 'prepatch', + 'preminor', + 'premajor' +]; + +export function toVersion(version: string): Version { + if (semver.valid(version)) { + return version; + } else { + throw new Error( + `Invalid semver version: ${version} (See https://github.com/npm/node-semver)` + ); + } +} + +export function getPrereleaseType(version: Version): string | null { + let parts = semver.prerelease(version); + if (parts) return parts[0]; + return null; +} + +export function increment(version: Version, type: IncrementType) { + let prereleaseType = getPrereleaseType(version) || 'beta'; + return semver.inc(version, type, prereleaseType); +} + +const MESSSAGES = { + patch: ver => messages.patch(increment(ver, 'patch')), + minor: ver => messages.minor(increment(ver, 'minor')), + major: ver => messages.major(increment(ver, 'major')), + prepatch: ver => messages.prepatch(increment(ver, 'prepatch')), + preminor: ver => messages.preminor(increment(ver, 'preminor')), + premajor: ver => messages.premajor(increment(ver, 'premajor')), + prerelease: ver => messages.prerelease(increment(ver, 'prerelease')) +}; + +export function getIncrementMessage( + currentVersion: Version, + type: IncrementType +): messages.Message { + return MESSSAGES[type](currentVersion); +} diff --git a/src/utils/yarn.js b/src/utils/yarn.js index fdadee18..b8e2c97c 100644 --- a/src/utils/yarn.js +++ b/src/utils/yarn.js @@ -2,7 +2,7 @@ import includes from 'array-includes'; import projectBinPath from 'project-bin-path'; import * as path from 'path'; -import type { Dependency, configDependencyType } from '../types'; +import type { Dependency, ConfigDependencyType } from '../types'; import type Package from '../Package'; import Project from '../Project'; import * as processes from './processes'; @@ -25,7 +25,7 @@ function depTypeToFlag(depType) { export async function add( pkg: Package, dependencies: Array<Dependency>, - type?: configDependencyType + type?: ConfigDependencyType ) { let localYarn = path.join(await getLocalBinPath(), 'yarn'); let spawnArgs = ['add']; diff --git a/yarn.lock b/yarn.lock index 68aaf471..415667b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1211,6 +1211,10 @@ detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" +diff@^3.1.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" + diff@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" @@ -1352,7 +1356,7 @@ extend@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -external-editor@^2.0.4: +external-editor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48" dependencies: @@ -1442,9 +1446,9 @@ fixturez@^1.1.0: signal-exit "^3.0.2" tempy "^0.2.1" -flow-bin@^0.63.1: - version "0.63.1" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.63.1.tgz#ab00067c197169a5fb5b4996c8f6927b06694828" +flow-bin@^0.66.0: + version "0.66.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.66.0.tgz#a96dde7015dc3343fd552a7b4963c02be705ca26" for-in@^1.0.1: version "1.0.2" @@ -1472,6 +1476,12 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +formatio@1.2.0, formatio@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" + dependencies: + samsam "1.x" + fs-extra@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" @@ -1736,15 +1746,15 @@ ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" -inquirer@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" +inquirer@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-4.0.2.tgz#cc678b4cbc0e183a3500cc63395831ec956ab0a3" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.0" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^2.0.4" + external-editor "^2.1.0" figures "^2.0.0" lodash "^4.3.0" mute-stream "0.0.7" @@ -1913,6 +1923,10 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2336,6 +2350,10 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" +just-extend@^1.1.26: + version "1.1.27" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" + kind-of@^3.0.2: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -2470,6 +2488,10 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2493,6 +2515,14 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" +lolex@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + +lolex@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.1.tgz#3d2319894471ea0950ef64692ead2a5318cff362" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -2647,6 +2677,16 @@ natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" +nise@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.2.0.tgz#079d6cadbbcb12ba30e38f1c999f36ad4d6baa53" + dependencies: + formatio "^1.2.0" + just-extend "^1.1.26" + lolex "^1.6.0" + path-to-regexp "^1.7.0" + text-encoding "^0.6.4" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -2898,6 +2938,12 @@ path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -3264,6 +3310,10 @@ safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +samsam@1.x: + version "1.3.0" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" + sane@~1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/sane/-/sane-1.6.0.tgz#9610c452307a135d29c1fdfe2547034180c46775" @@ -3319,6 +3369,18 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +sinon@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.1.4.tgz#36bb237bae38ddf9cc92dcc1b16c51e7785bbc9c" + dependencies: + diff "^3.1.0" + formatio "1.2.0" + lodash.get "^4.4.2" + lolex "^2.2.0" + nise "^1.2.0" + supports-color "^4.4.0" + type-detect "^4.0.5" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -3503,6 +3565,12 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" +supports-color@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + symbol-observable@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" @@ -3533,8 +3601,8 @@ tar@^2.2.1: inherits "2" task-graph-runner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/task-graph-runner/-/task-graph-runner-1.0.1.tgz#321eb31f06b915dd9504e369187d11c14ddc6b26" + version "1.0.2" + resolved "https://registry.yarnpkg.com/task-graph-runner/-/task-graph-runner-1.0.2.tgz#dfc73e4f92d74b974a854ccd3b2a8c7f1fbf4137" dependencies: array-includes "^3.0.3" @@ -3570,6 +3638,10 @@ test-exclude@^4.1.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" +text-encoding@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + throat@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/throat/-/throat-3.2.0.tgz#50cb0670edbc40237b9e347d7e1f88e4620af836" @@ -3632,6 +3704,10 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2" + typeable-promisify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/typeable-promisify/-/typeable-promisify-2.0.1.tgz#1baee82abaf13280198eb11e98589c881a6bd80d"