From b81d9b151aadbd2fa28e510a37a084bb6811bbed Mon Sep 17 00:00:00 2001 From: Jamie Mason Date: Thu, 4 Oct 2018 21:26:46 +0100 Subject: [PATCH] feat(cli): specify dependency types as options --- src/bin-fix-mismatches.ts | 23 +-- src/bin-format.ts | 23 +-- src/bin-list-mismatches.ts | 26 +-- src/bin-list.ts | 26 +-- src/bin-set-semver-ranges.ts | 25 +-- src/bin.ts | 19 +- src/constants.ts | 15 ++ src/fix-mismatches.ts | 58 ++++++ .../manifest-data => }/format.spec.ts | 10 +- src/format.ts | 93 +++++++++ src/lib/collect.ts | 2 + src/lib/get-dependency-types.ts | 15 ++ src/lib/get-packages.ts | 20 ++ src/lib/get-versions-by-name.ts | 54 +++++ src/{ => lib}/version.spec.ts | 0 src/{ => lib}/version.ts | 23 +-- src/list-mismatches.ts | 37 ++++ src/list.ts | 34 ++++ src/manifests/get-manifests.spec.ts | 35 ---- src/manifests/get-manifests.ts | 28 --- src/manifests/index.spec.ts | 187 ------------------ src/manifests/index.ts | 84 -------- src/manifests/manifest-data/format.ts | 70 ------- src/manifests/manifest-data/index.spec.ts | 143 -------------- src/manifests/manifest-data/index.ts | 134 ------------- src/set-semver-ranges.ts | 66 +++++++ src/syncpack.ts | 21 ++ tsconfig.json | 2 +- 28 files changed, 443 insertions(+), 830 deletions(-) create mode 100644 src/fix-mismatches.ts rename src/{manifests/manifest-data => }/format.spec.ts (90%) create mode 100644 src/format.ts create mode 100644 src/lib/collect.ts create mode 100644 src/lib/get-dependency-types.ts create mode 100644 src/lib/get-packages.ts create mode 100644 src/lib/get-versions-by-name.ts rename src/{ => lib}/version.spec.ts (100%) rename src/{ => lib}/version.ts (65%) create mode 100644 src/list-mismatches.ts create mode 100644 src/list.ts delete mode 100644 src/manifests/get-manifests.spec.ts delete mode 100644 src/manifests/get-manifests.ts delete mode 100644 src/manifests/index.spec.ts delete mode 100644 src/manifests/index.ts delete mode 100644 src/manifests/manifest-data/format.ts delete mode 100644 src/manifests/manifest-data/index.spec.ts delete mode 100644 src/manifests/manifest-data/index.ts create mode 100644 src/set-semver-ranges.ts create mode 100644 src/syncpack.ts diff --git a/src/bin-fix-mismatches.ts b/src/bin-fix-mismatches.ts index 1c04b840..bcf16040 100644 --- a/src/bin-fix-mismatches.ts +++ b/src/bin-fix-mismatches.ts @@ -1,25 +1,6 @@ #!/usr/bin/env node -import chalk from 'chalk'; import program = require('commander'); -import _ = require('lodash'); -import { relative } from 'path'; -import { OPTION_SOURCES } from './constants'; -import { setVersionsToNewestMismatch } from './manifests'; +import { run } from './fix-mismatches'; -const collect = (value: string, values: string[] = []) => values.concat(value); - -program - .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) - .parse(process.argv); - -const sources: string[] = - program.source && program.source.length - ? program.source - : OPTION_SOURCES.default; - -setVersionsToNewestMismatch(...sources).then((descriptors) => { - _.each(descriptors, (descriptor) => { - console.log(chalk.blue(`./${relative('.', descriptor.path)}`)); - }); -}); +run(program); diff --git a/src/bin-format.ts b/src/bin-format.ts index 759c5473..c83c9e1f 100644 --- a/src/bin-format.ts +++ b/src/bin-format.ts @@ -1,25 +1,6 @@ #!/usr/bin/env node -import chalk from 'chalk'; import program = require('commander'); -import _ = require('lodash'); -import { relative } from 'path'; -import { OPTION_SOURCES } from './constants'; -import { format } from './manifests'; +import { run } from './format'; -const collect = (value: string, values: string[] = []) => values.concat(value); - -program - .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) - .parse(process.argv); - -const sources: string[] = - program.source && program.source.length - ? program.source - : OPTION_SOURCES.default; - -format(...sources).then((descriptors) => { - _.each(descriptors, (descriptor) => { - console.log(chalk.blue(`./${relative('.', descriptor.path)}`)); - }); -}); +run(program); diff --git a/src/bin-list-mismatches.ts b/src/bin-list-mismatches.ts index 855ba308..58ccecb7 100644 --- a/src/bin-list-mismatches.ts +++ b/src/bin-list-mismatches.ts @@ -1,28 +1,6 @@ #!/usr/bin/env node -import chalk from 'chalk'; import program = require('commander'); -import _ = require('lodash'); -import { OPTION_SOURCES } from './constants'; -import { getMismatchedVersions } from './manifests'; +import { run } from './list-mismatches'; -const collect = (value: string, values: string[] = []) => values.concat(value); - -program - .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) - .parse(process.argv); - -const sources: string[] = - program.source && program.source.length - ? program.source - : OPTION_SOURCES.default; - -getMismatchedVersions(...sources).then((versionByName) => { - _.each(versionByName, (versions, name) => { - console.log(chalk.yellow(name), chalk.dim(versions.join(', '))); - }); - - if (versionByName.length) { - process.exit(1); - } -}); +run(program); diff --git a/src/bin-list.ts b/src/bin-list.ts index 9e2e9fc2..8b32973c 100644 --- a/src/bin-list.ts +++ b/src/bin-list.ts @@ -1,28 +1,6 @@ #!/usr/bin/env node -import chalk from 'chalk'; import program = require('commander'); -import _ = require('lodash'); -import { OPTION_SOURCES } from './constants'; -import { getVersions } from './manifests'; +import { run } from './list'; -const collect = (value: string, values: string[] = []) => values.concat(value); - -program - .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) - .parse(process.argv); - -const sources: string[] = - program.source && program.source.length - ? program.source - : OPTION_SOURCES.default; - -getVersions(...sources).then((versionByName) => { - _.each(versionByName, (versions, name) => { - if (versions.length > 1) { - console.log(chalk.yellow(name), chalk.dim(versions.join(', '))); - } else { - console.log(chalk.blue(name), chalk.dim(versions[0])); - } - }); -}); +run(program); diff --git a/src/bin-set-semver-ranges.ts b/src/bin-set-semver-ranges.ts index 421428bb..8a05e045 100644 --- a/src/bin-set-semver-ranges.ts +++ b/src/bin-set-semver-ranges.ts @@ -1,27 +1,6 @@ #!/usr/bin/env node -import chalk from 'chalk'; import program = require('commander'); -import _ = require('lodash'); -import { relative } from 'path'; -import { OPTION_SEMVER_RANGE, OPTION_SOURCES } from './constants'; -import { setVersionRange } from './manifests'; +import { run } from './set-semver-ranges'; -const collect = (value: string, values: string[] = []) => values.concat(value); - -program - .option(OPTION_SEMVER_RANGE.spec, OPTION_SEMVER_RANGE.description) - .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) - .parse(process.argv); - -const semverRange: string = program.semverRange || OPTION_SEMVER_RANGE.default; -const sources: string[] = - program.source && program.source.length - ? program.source - : OPTION_SOURCES.default; - -setVersionRange(semverRange, ...sources).then((descriptors) => { - _.each(descriptors, (descriptor) => { - console.log(chalk.blue(`./${relative('.', descriptor.path)}`)); - }); -}); +run(program); diff --git a/src/bin.ts b/src/bin.ts index 1a274ae3..b0417dd2 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,21 +1,6 @@ #!/usr/bin/env node import program = require('commander'); -import { - FIX_MISMATCHES, - FORMAT, - LIST, - LIST_MISMATCHES, - SET_SEMVER_RANGES, - VERSION -} from './constants'; +import { run } from './syncpack'; -program - .version(VERSION) - .command(FIX_MISMATCHES.command, FIX_MISMATCHES.description) - .command(FORMAT.command, FORMAT.description) - .command(LIST.command, LIST.description, { isDefault: true }) - .command(LIST_MISMATCHES.command, LIST_MISMATCHES.description) - .command(SET_SEMVER_RANGES.command, SET_SEMVER_RANGES.description); - -program.parse(process.argv); +run(program); diff --git a/src/constants.ts b/src/constants.ts index e4081fc4..b3ac569b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -85,3 +85,18 @@ export const OPTION_SOURCES = { description: 'glob pattern for package.json files to read from', spec: '-s, --source [pattern]' }; + +export const OPTIONS_PROD = { + description: 'include dependencies', + spec: '-p, --prod' +}; + +export const OPTIONS_DEV = { + description: 'include devDependencies', + spec: '-d, --dev' +}; + +export const OPTIONS_PEER = { + description: 'include peerDependencies', + spec: '-P, --peer' +}; diff --git a/src/fix-mismatches.ts b/src/fix-mismatches.ts new file mode 100644 index 00000000..67343038 --- /dev/null +++ b/src/fix-mismatches.ts @@ -0,0 +1,58 @@ +import chalk from 'chalk'; +import { CommanderStatic } from 'commander'; +import _ = require('lodash'); +import { relative } from 'path'; +import { + OPTION_SOURCES, + OPTIONS_DEV, + OPTIONS_PEER, + OPTIONS_PROD +} from './constants'; +import { collect } from './lib/collect'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getPackages } from './lib/get-packages'; +import { getMismatchedVersionsByName } from './lib/get-versions-by-name'; +import { getNewest } from './lib/version'; +import { writeJson } from './lib/write-json'; + +export const run = async (program: CommanderStatic) => { + program + .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) + .option(OPTIONS_PROD.spec, OPTIONS_PROD.description) + .option(OPTIONS_DEV.spec, OPTIONS_DEV.description) + .option(OPTIONS_PEER.spec, OPTIONS_PEER.description) + .parse(process.argv); + + const dependencyTypes = getDependencyTypes(program); + const pkgs = await getPackages(program); + const mismatchedVersionsByName = getMismatchedVersionsByName( + dependencyTypes, + pkgs + ); + + Object.entries(mismatchedVersionsByName).forEach(([name, versions]) => { + const newest = getNewest(versions); + if (typeof newest === 'string') { + pkgs.forEach(({ data, path }) => { + dependencyTypes.forEach((type) => { + if (data[type] && data[type][name] && data[type][name] !== newest) { + console.log( + relative(process.cwd(), path), + name, + data[type][name], + '->', + newest + ); + data[type][name] = newest; + } + }); + }); + } + }); + + await Promise.all(pkgs.map(({ data, path }) => writeJson(path, data))); + + _.each(pkgs, (pkg) => { + console.log(chalk.blue(`./${relative('.', pkg.path)}`)); + }); +}; diff --git a/src/manifests/manifest-data/format.spec.ts b/src/format.spec.ts similarity index 90% rename from src/manifests/manifest-data/format.spec.ts rename to src/format.spec.ts index d5bdb671..d2737342 100644 --- a/src/manifests/manifest-data/format.spec.ts +++ b/src/format.spec.ts @@ -1,8 +1,8 @@ -import { getUntidyManifest } from '../../../test/fixtures'; -import { shuffleObject } from '../../../test/helpers'; -import { SORT_FIRST } from '../../constants'; -import { IManifest } from '../../typings'; -import { manifestData } from './index'; +import { getUntidyManifest } from '../test/fixtures'; +import { shuffleObject } from '../test/helpers'; +import { SORT_FIRST } from './constants'; +import { IManifest } from './typings'; +import { manifestData } from './manifests/manifest-data'; describe('format', () => { let results: IManifest[]; diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 00000000..dbf14b52 --- /dev/null +++ b/src/format.ts @@ -0,0 +1,93 @@ +import chalk from 'chalk'; +import { CommanderStatic } from 'commander'; +import _ = require('lodash'); +import { relative } from 'path'; +import { OPTION_SOURCES, SORT_AZ, SORT_FIRST } from './constants'; +import { collect } from './lib/collect'; +import { getPackages } from './lib/get-packages'; +import { writeJson } from './lib/write-json'; +import { IManifest } from './typings'; + +export const run = async (program: CommanderStatic) => { + const shortenBugs = (manifest: IManifest): IManifest => { + if ( + manifest.bugs && + typeof manifest.bugs === 'object' && + manifest.bugs.url + ) { + return { + ...manifest, + bugs: manifest.bugs.url + }; + } + return manifest; + }; + + const shortenRepository = (manifest: IManifest): IManifest => { + if ( + manifest.repository && + typeof manifest.repository === 'object' && + manifest.repository.url && + manifest.repository.url.indexOf('github.com') !== -1 + ) { + return { + ...manifest, + repository: manifest.repository.url.split('github.com/')[1] + }; + } + return manifest; + }; + + const sortObject = (obj: IManifest) => + _(obj) + .entries() + .sortBy('0') + .reduce((next, [key, value]) => ({ ...next, [key]: value }), {}); + + const sortValue = (value: any) => + _.isArray(value) + ? value.slice(0).sort() + : _.isObject(value) + ? sortObject(value) + : value; + + const sortManifest = (manifest: IManifest): IManifest => { + const [first, rest] = _(manifest) + .entries() + .sortBy('0') + .partition(([key, value]) => SORT_FIRST.indexOf(key) !== -1) + .value(); + + const firstSorted = [...first].sort( + ([keyA], [keyB]) => SORT_FIRST.indexOf(keyA) - SORT_FIRST.indexOf(keyB) + ); + + const restSorted = _(rest) + .map(([key, value]) => [ + key, + SORT_AZ.indexOf(key) !== -1 ? sortValue(value) : value + ]) + .value(); + + return _([...firstSorted, ...restSorted]).reduce( + (obj, [key, value]) => ({ ...obj, [key]: value }), + {} as IManifest + ); + }; + + program + .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) + .parse(process.argv); + + const pkgs = await getPackages(program); + + await Promise.all( + pkgs.map(({ data, path }) => + writeJson(path, sortManifest(shortenBugs(shortenRepository(data)))) + ) + ); + + _.each(pkgs, (pkg) => { + console.log(chalk.blue(`./${relative('.', pkg.path)}`)); + }); +}; diff --git a/src/lib/collect.ts b/src/lib/collect.ts new file mode 100644 index 00000000..3db26bc9 --- /dev/null +++ b/src/lib/collect.ts @@ -0,0 +1,2 @@ +export const collect = (value: string, values: string[] = []) => + values.concat(value); diff --git a/src/lib/get-dependency-types.ts b/src/lib/get-dependency-types.ts new file mode 100644 index 00000000..c14aec82 --- /dev/null +++ b/src/lib/get-dependency-types.ts @@ -0,0 +1,15 @@ +import { CommanderStatic } from 'commander'; +import { DEPENDENCY_TYPES } from '../constants'; +import { IManifestKey } from '../typings'; + +export type GetDependencyTypes = (program: CommanderStatic) => IManifestKey[]; + +export const getDependencyTypes: GetDependencyTypes = (program) => + program.prod || program.dev || program.peer + ? DEPENDENCY_TYPES.filter( + (type) => + (type === 'dependencies' && program.prod) || + (type === 'devDependencies' && program.dev) || + (type === 'peerDependencies' && program.peer) + ) + : DEPENDENCY_TYPES; diff --git a/src/lib/get-packages.ts b/src/lib/get-packages.ts new file mode 100644 index 00000000..43b94d7f --- /dev/null +++ b/src/lib/get-packages.ts @@ -0,0 +1,20 @@ +import { CommanderStatic } from 'commander'; +import fs = require('fs-extra'); +import globby = require('globby'); +import { OPTION_SOURCES } from '../constants'; +import { IManifestDescriptor } from '../typings'; + +export const getSources = (program: CommanderStatic): string[] => + program.source && program.source.length + ? program.source + : OPTION_SOURCES.default; + +export const getPackages = async ( + program: CommanderStatic +): Promise => + Promise.all( + (await globby(getSources(program))).map(async (filePath) => ({ + data: await fs.readJSON(filePath), + path: filePath + })) + ); diff --git a/src/lib/get-versions-by-name.ts b/src/lib/get-versions-by-name.ts new file mode 100644 index 00000000..1b9582e3 --- /dev/null +++ b/src/lib/get-versions-by-name.ts @@ -0,0 +1,54 @@ +import { IManifestDescriptor, IManifestKey } from '../typings'; + +export interface IVersionsByName { + [name: string]: string[]; +} + +export type GetVersionsByName = ( + dependencyTypes: IManifestKey[], + pkgs: IManifestDescriptor[] +) => IVersionsByName; + +export const getVersionsByName: GetVersionsByName = (dependencyTypes, pkgs) => { + const versionsByName: IVersionsByName = {}; + for (const type of dependencyTypes) { + for (const pkg of pkgs) { + const dependencies = pkg.data[type]; + if (dependencies) { + for (const name in dependencies) { + if (dependencies.hasOwnProperty(name)) { + const version = dependencies[name]; + versionsByName[name] = versionsByName[name] || []; + if (!versionsByName[name].includes(version)) { + versionsByName[name].push(version); + } + } + } + } + } + } + return versionsByName; +}; + +export const getMismatchedVersionsByName: GetVersionsByName = ( + dependencyTypes, + pkgs +) => { + const mismatchedVersionsByName: IVersionsByName = {}; + const versionsByName = getVersionsByName(dependencyTypes, pkgs); + for (const type of dependencyTypes) { + for (const pkg of pkgs) { + const dependencies = pkg.data[type]; + if (dependencies) { + for (const name in dependencies) { + if (dependencies.hasOwnProperty(name)) { + if (versionsByName[name].length > 1) { + mismatchedVersionsByName[name] = versionsByName[name]; + } + } + } + } + } + } + return mismatchedVersionsByName; +}; diff --git a/src/version.spec.ts b/src/lib/version.spec.ts similarity index 100% rename from src/version.spec.ts rename to src/lib/version.spec.ts diff --git a/src/version.ts b/src/lib/version.ts similarity index 65% rename from src/version.ts rename to src/lib/version.ts index 44d9e1ec..99a89193 100644 --- a/src/version.ts +++ b/src/lib/version.ts @@ -1,20 +1,20 @@ import _ = require('lodash'); import semver = require('semver'); -import { GREATER, LESSER, SAME, SEMVER_ORDER } from './constants'; +import { GREATER, LESSER, SAME, SEMVER_ORDER } from '../constants'; -export type GetNewest = (versions: string[]) => string | undefined; -export type GetVersionNumber = (version: string) => string; -export type GetVersionRange = (version: string) => string; -export type SortBySemver = (versions: string[]) => string[]; +export const getNewest = (versions: string[]): string | undefined => + _(sortBySemver(versions)).last(); + +export const isValid = (version: string): boolean => + semver.valid(version) !== null; -export const getVersionNumber: GetVersionNumber = (version) => +export const getVersionNumber = (version: string): string => version.slice(version.search(/[0-9]/), version.length); -export const getVersionRange: GetVersionRange = (version) => - version.split(/[0-9]/)[0]; -export const isValid = (version: string) => semver.valid(version) !== null; +export const getVersionRange = (version: string): string => + version.split(/[0-9]/)[0]; -export const sortBySemver: SortBySemver = (versions: string[]) => +export const sortBySemver = (versions: string[]): string[] => versions .concat() .sort() @@ -54,6 +54,3 @@ export const sortBySemver: SortBySemver = (versions: string[]) => } return SAME; }); - -export const getNewest: GetNewest = (versions: string[]) => - _(sortBySemver(versions)).last(); diff --git a/src/list-mismatches.ts b/src/list-mismatches.ts new file mode 100644 index 00000000..126f2e6b --- /dev/null +++ b/src/list-mismatches.ts @@ -0,0 +1,37 @@ +import chalk from 'chalk'; +import { CommanderStatic } from 'commander'; +import _ = require('lodash'); +import { + OPTION_SOURCES, + OPTIONS_DEV, + OPTIONS_PEER, + OPTIONS_PROD +} from './constants'; +import { collect } from './lib/collect'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getPackages } from './lib/get-packages'; +import { getMismatchedVersionsByName } from './lib/get-versions-by-name'; + +export const run = async (program: CommanderStatic) => { + program + .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) + .option(OPTIONS_PROD.spec, OPTIONS_PROD.description) + .option(OPTIONS_DEV.spec, OPTIONS_DEV.description) + .option(OPTIONS_PEER.spec, OPTIONS_PEER.description) + .parse(process.argv); + + const dependencyTypes = getDependencyTypes(program); + const pkgs = await getPackages(program); + const mismatchedVersionsByName = getMismatchedVersionsByName( + dependencyTypes, + pkgs + ); + + _.each(mismatchedVersionsByName, (versions, name) => { + console.log(chalk.yellow(name), chalk.dim(versions.join(', '))); + }); + + if (Object.keys(mismatchedVersionsByName).length) { + process.exit(1); + } +}; diff --git a/src/list.ts b/src/list.ts new file mode 100644 index 00000000..0a49db75 --- /dev/null +++ b/src/list.ts @@ -0,0 +1,34 @@ +import chalk from 'chalk'; +import { CommanderStatic } from 'commander'; +import _ = require('lodash'); +import { + OPTION_SOURCES, + OPTIONS_DEV, + OPTIONS_PEER, + OPTIONS_PROD +} from './constants'; +import { collect } from './lib/collect'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getPackages } from './lib/get-packages'; +import { getVersionsByName } from './lib/get-versions-by-name'; + +export const run = async (program: CommanderStatic) => { + program + .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) + .option(OPTIONS_PROD.spec, OPTIONS_PROD.description) + .option(OPTIONS_DEV.spec, OPTIONS_DEV.description) + .option(OPTIONS_PEER.spec, OPTIONS_PEER.description) + .parse(process.argv); + + const dependencyTypes = getDependencyTypes(program); + const pkgs = await getPackages(program); + const versionsByName = getVersionsByName(dependencyTypes, pkgs); + + _.each(versionsByName, (versions, name) => { + if (versions.length > 1) { + console.log(chalk.yellow(name), chalk.dim(versions.join(', '))); + } else { + console.log(chalk.blue(name), chalk.dim(versions[0])); + } + }); +}; diff --git a/src/manifests/get-manifests.spec.ts b/src/manifests/get-manifests.spec.ts deleted file mode 100644 index 47a6e0b7..00000000 --- a/src/manifests/get-manifests.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import mock = require('mock-fs'); -import { createManifest, createMockFs } from '../../test/helpers'; -import { getManifests } from './get-manifests'; - -const pattern = '/Users/you/Dev/monorepo/packages/*/package.json'; - -afterEach(() => { - mock.restore(); -}); - -beforeEach(() => { - mock({ - ...createMockFs('foo', { chalk: '1.0.0' }), - ...createMockFs('bar', { rimraf: '0.1.2' }), - '/Users/you/Dev/monorepo/packages/invalid/package.json': JSON.stringify({ - some: 'incorrect shape' - }) - }); -}); - -describe('getManifests', () => { - it('returns all valid package.json which match the provided globs', async () => { - const result = await getManifests(pattern); - expect(result).toEqual([ - { - data: { dependencies: { rimraf: '0.1.2' }, name: 'bar' }, - path: '/Users/you/Dev/monorepo/packages/bar/package.json' - }, - { - data: { dependencies: { chalk: '1.0.0' }, name: 'foo' }, - path: '/Users/you/Dev/monorepo/packages/foo/package.json' - } - ]); - }); -}); diff --git a/src/manifests/get-manifests.ts b/src/manifests/get-manifests.ts deleted file mode 100644 index 81e43369..00000000 --- a/src/manifests/get-manifests.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { readJson } from 'fs-extra'; -import globby = require('globby'); -import { IFileDescriptor, IManifestDescriptor } from '../typings'; -import { manifestData } from './manifest-data'; - -type GetDescriptor = (path: string) => Promise; -type GetDescriptors = (paths: string[]) => Promise; -type FilterDescriptors = ( - descriptors: IFileDescriptor[] -) => IManifestDescriptor[]; - -const { isManifest } = manifestData; - -const getDescriptor: GetDescriptor = (path) => - readJson(path).then((data) => ({ data, path })); -const getDescriptors: GetDescriptors = (paths) => - Promise.all(paths.map(getDescriptor)); -const filterDescriptors: FilterDescriptors = (descriptors) => - descriptors - .filter((descriptor) => isManifest(descriptor.data)) - .map((descriptor) => descriptor as IManifestDescriptor); - -export const getManifests = ( - ...patterns: string[] -): Promise => - globby(patterns, { absolute: true }) - .then(getDescriptors) - .then(filterDescriptors); diff --git a/src/manifests/index.spec.ts b/src/manifests/index.spec.ts deleted file mode 100644 index 1258dc82..00000000 --- a/src/manifests/index.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { readFileSync } from 'fs'; -import _ = require('lodash'); -import mock = require('mock-fs'); -import { - createFile, - createManifest, - createMockDescriptor, - createMockFs -} from '../../test/helpers'; -import { IManifest, IManifestDescriptor } from '../typings'; -import { - format, - getMismatchedVersions, - getVersions, - setVersion, - setVersionRange, - setVersionsToNewestMismatch -} from './index'; - -const pattern = '/Users/you/Dev/monorepo/packages/*/package.json'; - -afterEach(() => { - mock.restore(); -}); - -beforeEach(() => { - mock({ - ...createMockFs( - 'foo', - { chalk: '2.3.0', commander: '2.13.0' }, - { jest: '22.1.3', prettier: '1.10.2', rimraf: '2.6.2' }, - { gulp: '3.9.1' } - ), - ...createMockFs('bar', { chalk: '1.0.0' }, { jest: '22.1.4' }), - ...createMockFs( - 'baz', - null, - { npm: 'https://github.com/npm/npm.git', prettier: '1.10.2' }, - { gulp: '*' } - ) - }); -}); - -describe('format', () => { - it('sorts and shortens properties according to a convention', async () => { - const result = await format(pattern); - const getByName = (name) => - Object.keys(_.find(result, (v) => v.data.name === name).data); - expect(getByName('foo')).toEqual([ - 'name', - 'dependencies', - 'devDependencies', - 'peerDependencies' - ]); - expect(getByName('bar')).toEqual([ - 'name', - 'dependencies', - 'devDependencies' - ]); - expect(getByName('baz')).toEqual([ - 'name', - 'devDependencies', - 'peerDependencies' - ]); - }); -}); - -describe('getMismatchedVersions', () => { - it('returns an index of dependencies used with different versions', async () => { - const result = await getMismatchedVersions(pattern); - expect(result).toEqual({ - chalk: ['1.0.0', '2.3.0'], - gulp: ['*', '3.9.1'], - jest: ['22.1.3', '22.1.4'] - }); - }); -}); - -describe('getVersions', () => { - it('returns an index of every unique dependency in use', async () => { - const result = await getVersions(pattern); - expect(result).toEqual({ - chalk: ['1.0.0', '2.3.0'], - commander: ['2.13.0'], - gulp: ['*', '3.9.1'], - jest: ['22.1.3', '22.1.4'], - npm: ['https://github.com/npm/npm.git'], - prettier: ['1.10.2'], - rimraf: ['2.6.2'] - }); - }); -}); - -describe('setVersion', () => { - it('sets the version of a named dependency when it is found', async () => { - const result = await setVersion('jest', '25.0.0', pattern); - expect(result).toEqual( - expect.arrayContaining([ - createMockDescriptor( - 'foo', - { chalk: '2.3.0', commander: '2.13.0' }, - { jest: '25.0.0', prettier: '1.10.2', rimraf: '2.6.2' }, - { gulp: '3.9.1' } - ), - createMockDescriptor('bar', { chalk: '1.0.0' }, { jest: '25.0.0' }), - createMockDescriptor( - 'baz', - null, - { npm: 'https://github.com/npm/npm.git', prettier: '1.10.2' }, - { gulp: '*' } - ) - ]) - ); - }); -}); - -describe('setVersionRange', () => { - it('sets the version range of all semver dependencies', async () => { - const result = await setVersionRange('^', pattern); - expect(result).toEqual( - expect.arrayContaining([ - createMockDescriptor( - 'foo', - { chalk: '^2.3.0', commander: '^2.13.0' }, - { jest: '^22.1.3', prettier: '^1.10.2', rimraf: '^2.6.2' }, - { gulp: '^3.9.1' } - ), - createMockDescriptor('bar', { chalk: '^1.0.0' }, { jest: '^22.1.4' }), - createMockDescriptor( - 'baz', - null, - { npm: 'https://github.com/npm/npm.git', prettier: '^1.10.2' }, - { gulp: '*' } - ) - ]) - ); - }); -}); - -describe('setVersionsToNewestMismatch', () => { - it('sets all dependencies used with different versions to the newest of those versions', async () => { - const result = await setVersionsToNewestMismatch(pattern); - expect(result).toEqual( - expect.arrayContaining([ - createMockDescriptor( - 'foo', - { chalk: '2.3.0', commander: '2.13.0' }, - { jest: '22.1.4', prettier: '1.10.2', rimraf: '2.6.2' }, - { gulp: '*' } - ), - createMockDescriptor('bar', { chalk: '2.3.0' }, { jest: '22.1.4' }), - createMockDescriptor( - 'baz', - null, - { npm: 'https://github.com/npm/npm.git', prettier: '1.10.2' }, - { gulp: '*' } - ) - ]) - ); - }); - it('rewrites the updated manifests with the correct data', async () => { - await setVersionsToNewestMismatch(pattern); - expect( - readFileSync('/Users/you/Dev/monorepo/packages/foo/package.json', 'utf8') - ).toEqual( - createFile( - 'foo', - { chalk: '2.3.0', commander: '2.13.0' }, - { jest: '22.1.4', prettier: '1.10.2', rimraf: '2.6.2' }, - { gulp: '*' } - ) - ); - expect( - readFileSync('/Users/you/Dev/monorepo/packages/bar/package.json', 'utf8') - ).toEqual(createFile('bar', { chalk: '2.3.0' }, { jest: '22.1.4' })); - expect( - readFileSync('/Users/you/Dev/monorepo/packages/baz/package.json', 'utf8') - ).toEqual( - createFile( - 'baz', - null, - { npm: 'https://github.com/npm/npm.git', prettier: '1.10.2' }, - { gulp: '*' } - ) - ); - }); -}); diff --git a/src/manifests/index.ts b/src/manifests/index.ts deleted file mode 100644 index 90a74c68..00000000 --- a/src/manifests/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { writeJson } from '../lib/write-json'; -import { IDictionary, IManifest, IManifestDescriptor } from '../typings'; -import { getManifests } from './get-manifests'; -import { manifestData } from './manifest-data'; - -export type Format = (...sources: string[]) => Promise; -export type GetMismatchedVersions = ( - ...sources: string[] -) => Promise>; -export type GetVersions = ( - ...sources: string[] -) => Promise>; -export type SetVersion = ( - name: string, - version: string, - ...sources: string[] -) => Promise; -export type SetVersionRange = ( - range: string, - ...sources: string[] -) => Promise; -export type SetVersionsToNewestMismatch = ( - ...sources: string[] -) => Promise; - -const unwrap = (descriptors: IManifestDescriptor[]) => - descriptors.map((descriptor) => descriptor.data); - -const writeDescriptors = ( - descriptors: IManifestDescriptor[] -): Promise => - Promise.all( - descriptors.map((descriptor) => writeJson(descriptor.path, descriptor.data)) - ).then(() => descriptors); - -export const format: Format = (...sources) => - getManifests(...sources) - .then((descriptors) => { - const data = unwrap(descriptors); - const nextData = manifestData.format(data); - return descriptors.map((descriptor, i) => ({ - data: nextData[i], - path: descriptor.path - })); - }) - .then(writeDescriptors); - -export const getMismatchedVersions: GetMismatchedVersions = (...sources) => - getManifests(...sources) - .then(unwrap) - .then(manifestData.getMismatchedVersions); - -export const getVersions: GetVersions = (...sources) => - getManifests(...sources) - .then(unwrap) - .then(manifestData.getVersions); - -export const setVersion: SetVersion = (name, version, ...sources) => - getManifests(...sources).then((descriptors) => { - manifestData.setVersion(name, version, unwrap(descriptors)); - return descriptors; - }); - -export const setVersionRange: SetVersionRange = (range, ...sources) => - getManifests(...sources) - .then((descriptors) => { - manifestData.setVersionRange(range, unwrap(descriptors)); - return descriptors; - }) - .then(writeDescriptors); - -export const setVersionsToNewestMismatch: SetVersionsToNewestMismatch = ( - ...sources -) => - getManifests(...sources) - .then((descriptors) => { - const data = unwrap(descriptors); - const nextData = manifestData.setVersionsToNewestMismatch(data); - return descriptors.map((descriptor, i) => ({ - data: nextData[i], - path: descriptor.path - })); - }) - .then(writeDescriptors); diff --git a/src/manifests/manifest-data/format.ts b/src/manifests/manifest-data/format.ts deleted file mode 100644 index 8173afd8..00000000 --- a/src/manifests/manifest-data/format.ts +++ /dev/null @@ -1,70 +0,0 @@ -import _ = require('lodash'); -import { SORT_AZ, SORT_FIRST } from '../../constants'; -import { IManifest } from '../../typings'; - -export type Format = (manifests: IManifest[]) => IManifest[]; -export type ManifestMapper = (manifest: IManifest) => IManifest; - -const shortenBugs: ManifestMapper = (manifest: IManifest) => { - if (manifest.bugs && typeof manifest.bugs === 'object' && manifest.bugs.url) { - return { - ...manifest, - bugs: manifest.bugs.url - }; - } - return manifest; -}; - -const shortenRepository: ManifestMapper = (manifest) => { - if ( - manifest.repository && - typeof manifest.repository === 'object' && - manifest.repository.url && - manifest.repository.url.indexOf('github.com') !== -1 - ) { - return { - ...manifest, - repository: manifest.repository.url.split('github.com/')[1] - }; - } - return manifest; -}; - -const sortObject = (obj: IManifest) => - _(obj) - .entries() - .sortBy('0') - .reduce((next, [key, value]) => ({ ...next, [key]: value }), {}); - -const sortValue = (value: any) => - _.isArray(value) - ? value.slice(0).sort() - : _.isObject(value) - ? sortObject(value) - : value; - -const sortManifest: ManifestMapper = (manifest) => { - const [first, rest] = _(manifest) - .entries() - .sortBy('0') - .partition(([key, value]) => SORT_FIRST.indexOf(key) !== -1) - .value(); - const firstSorted = [...first].sort( - ([keyA], [keyB]) => SORT_FIRST.indexOf(keyA) - SORT_FIRST.indexOf(keyB) - ); - const restSorted = _(rest) - .map(([key, value]) => [ - key, - SORT_AZ.indexOf(key) !== -1 ? sortValue(value) : value - ]) - .value(); - return _([...firstSorted, ...restSorted]).reduce( - (obj, [key, value]) => ({ ...obj, [key]: value }), - {} as IManifest - ); -}; - -export const format: Format = (manifests) => - _.map(manifests, (manifest) => - sortManifest(shortenBugs(shortenRepository(manifest))) - ); diff --git a/src/manifests/manifest-data/index.spec.ts b/src/manifests/manifest-data/index.spec.ts deleted file mode 100644 index 026f72e9..00000000 --- a/src/manifests/manifest-data/index.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { - getAnyVersionManifests, - getExactVersionManifests, - getGreaterThanOrEqualVersionManifests, - getGreaterThanVersionManifests, - getLessThanOrEqualVersionManifests, - getLessThanVersionManifests, - getLooseVersionManifests, - getMinorVersionManifests, - getPatchVersionManifests -} from '../../../test/fixtures'; -import { createManifest } from '../../../test/helpers'; -import { - RANGE_ANY, - RANGE_EXACT, - RANGE_GT, - RANGE_GTE, - RANGE_LOOSE, - RANGE_LT, - RANGE_LTE, - RANGE_MINOR, - RANGE_PATCH -} from '../../constants'; -import { IManifest } from '../../typings'; -import { manifestData } from './index'; - -const { - getMismatchedVersions, - getVersions, - setVersion, - setVersionRange, - setVersionsToNewestMismatch -} = manifestData; - -describe('getMismatchedVersions', () => { - it('returns an index of dependencies used with different versions', () => { - expect(getMismatchedVersions(getExactVersionManifests())).toEqual({ - chalk: ['1.0.0', '2.3.0'], - gulp: ['*', '0.9.1'], - jest: ['22.1.3', '22.1.4'] - }); - }); -}); - -describe('getVersions', () => { - it('returns an index of every unique dependency in use', () => { - expect(getVersions(getExactVersionManifests())).toEqual({ - chalk: ['1.0.0', '2.3.0'], - commander: ['2.13.0'], - gulp: ['*', '0.9.1'], - jest: ['22.1.3', '22.1.4'], - npm: ['https://github.com/npm/npm.git'], - prettier: ['1.10.2'], - rimraf: ['2.6.2'] - }); - }); -}); - -describe('setVersion', () => { - it('sets the version of a named dependency when it is found', () => { - expect(setVersion('jest', '25.0.0', getExactVersionManifests())).toEqual([ - createManifest( - 'foo', - { chalk: '2.3.0', commander: '2.13.0' }, - { jest: '25.0.0', prettier: '1.10.2', rimraf: '2.6.2' }, - { gulp: '0.9.1' } - ), - createManifest('bar', { chalk: '1.0.0' }, { jest: '25.0.0' }), - createManifest( - 'baz', - null, - { npm: 'https://github.com/npm/npm.git', prettier: '1.10.2' }, - { gulp: '*' } - ) - ]); - }); -}); - -describe('setVersionRange', () => { - const assertRange = (inputRange: string, expectedManifests: IManifest[]) => { - [ - getExactVersionManifests(), - getGreaterThanVersionManifests(), - getGreaterThanOrEqualVersionManifests(), - getLessThanVersionManifests(), - getLessThanOrEqualVersionManifests(), - getMinorVersionManifests(), - getPatchVersionManifests() - ].forEach((inputManifests) => { - expect(setVersionRange(inputRange, inputManifests)).toEqual( - expectedManifests - ); - }); - }; - - it(`sets semver ranges to the "${RANGE_ANY}" format`, () => { - assertRange(RANGE_ANY, getAnyVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_EXACT}" format`, () => { - assertRange(RANGE_EXACT, getExactVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_GT}" format`, () => { - assertRange(RANGE_GT, getGreaterThanVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_GTE}" format`, () => { - assertRange(RANGE_GTE, getGreaterThanOrEqualVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_LOOSE}" format`, () => { - assertRange(RANGE_LOOSE, getLooseVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_LT}" format`, () => { - assertRange(RANGE_LT, getLessThanVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_LTE}" format`, () => { - assertRange(RANGE_LTE, getLessThanOrEqualVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_MINOR}" format`, () => { - assertRange(RANGE_MINOR, getMinorVersionManifests()); - }); - it(`sets semver ranges to the "${RANGE_PATCH}" format`, () => { - assertRange(RANGE_PATCH, getPatchVersionManifests()); - }); -}); - -describe('setVersionsToNewestMismatch', () => { - it('sets the version of dependencies with different versions to the newest of those versions found', () => { - expect(setVersionsToNewestMismatch(getExactVersionManifests())).toEqual([ - createManifest( - 'foo', - { chalk: '2.3.0', commander: '2.13.0' }, - { jest: '22.1.4', prettier: '1.10.2', rimraf: '2.6.2' }, - { gulp: '*' } - ), - createManifest('bar', { chalk: '2.3.0' }, { jest: '22.1.4' }), - createManifest( - 'baz', - null, - { npm: 'https://github.com/npm/npm.git', prettier: '1.10.2' }, - { gulp: '*' } - ) - ]); - }); -}); diff --git a/src/manifests/manifest-data/index.ts b/src/manifests/manifest-data/index.ts deleted file mode 100644 index 1e450dc2..00000000 --- a/src/manifests/manifest-data/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -import _ = require('lodash'); -import semver = require('semver'); -import { DEPENDENCY_TYPES, RANGE_ANY, RANGE_LOOSE } from '../../constants'; -import { IDictionary, IManifest } from '../../typings'; -import { getNewest, getVersionNumber } from '../../version'; -import { format } from './format'; - -export type GetMismatchedVersions = ( - manifests: IManifest[] -) => IDictionary; -export type GetVersions = (manifests: IManifest[]) => IDictionary; -export type SetVersion = ( - name: string, - version: string, - manifests: IManifest[] -) => IManifest[]; -export type SetVersionRange = ( - range: string, - manifests: IManifest[] -) => IManifest[]; -export type SetVersionsToNewestMismatch = ( - manifests: IManifest[] -) => IManifest[]; - -const isObject = (value: any) => Boolean(value && typeof value === 'object'); -const join = ({ name, version }: IDictionary) => `${name}@${version}`; -const gatherDependencies = (manifest: IManifest) => - _.chain(DEPENDENCY_TYPES) - .map((property) => manifest[property]) - .filter(Boolean) - .flatMap((dependencies) => - _.map(dependencies, (version, name) => ({ name, version })) - ) - .value(); - -const isManifest = (value: any): boolean => - Boolean( - isObject(value) && - 'name' in value && - ('dependencies' in value || - 'devDependencies' in value || - 'peerDependencies' in value) - ); - -const getMismatchedVersions: GetMismatchedVersions = (manifests) => - _.chain(manifests) - .map(gatherDependencies) - .flatten() - .uniqBy(join) - .sortBy(join) - .groupBy('name') - .reduce((index: IDictionary, dependencies, name) => { - if (dependencies.length > 1) { - index[name] = dependencies.map(_.property('version')); - } - return index; - }, {}) - .value(); - -const getVersions: GetVersions = (manifests) => - _.chain(manifests) - .map(gatherDependencies) - .flatten() - .uniqBy(join) - .sortBy(join) - .groupBy('name') - .reduce((index: IDictionary, dependencies, name) => { - index[name] = dependencies.map(_.property('version')); - return index; - }, {}) - .value(); - -const setVersion: SetVersion = (name, version, manifests) => { - _(manifests).each((manifest) => - _(DEPENDENCY_TYPES) - .map((property) => manifest[property]) - .filter(Boolean) - .filter((dependencies) => name in dependencies) - .each((dependencies) => { - dependencies[name] = version; - }) - ); - return manifests; -}; - -const setVersionRange: SetVersionRange = (range, manifests) => { - _(manifests).each((manifest) => - _(DEPENDENCY_TYPES) - .map((property) => manifest[property]) - .filter(Boolean) - .each((dependencies) => { - _(dependencies).each((version, name) => { - const versionNumber = getVersionNumber(version); - if (version !== '*' && semver.validRange(version)) { - if (range === RANGE_ANY) { - dependencies[name] = '*'; - } else if (range === RANGE_LOOSE) { - dependencies[name] = - semver.major(versionNumber) === 0 - ? `${semver.major(versionNumber)}.${semver.minor( - versionNumber - )}.x` - : `${semver.major(versionNumber)}.x.x`; - } else { - dependencies[name] = `${range}${versionNumber}`; - } - } - }); - }) - ); - return manifests; -}; - -const setVersionsToNewestMismatch: SetVersionsToNewestMismatch = ( - manifests -) => { - _(getMismatchedVersions(manifests)) - .map((versions, name) => ({ name, newest: getNewest(versions) })) - .filter(({ newest }) => typeof newest === 'string') - .each(({ name, newest }) => { - setVersion(name, newest as string, manifests); - }); - return manifests; -}; - -export const manifestData = { - format, - getMismatchedVersions, - getVersions, - isManifest, - setVersion, - setVersionRange, - setVersionsToNewestMismatch -}; diff --git a/src/set-semver-ranges.ts b/src/set-semver-ranges.ts new file mode 100644 index 00000000..7ce8efed --- /dev/null +++ b/src/set-semver-ranges.ts @@ -0,0 +1,66 @@ +import chalk from 'chalk'; +import { CommanderStatic } from 'commander'; +import _ = require('lodash'); +import { relative } from 'path'; +import semver = require('semver'); +import { + OPTION_SEMVER_RANGE, + OPTION_SOURCES, + OPTIONS_DEV, + OPTIONS_PEER, + OPTIONS_PROD, + RANGE_ANY, + RANGE_LOOSE +} from './constants'; +import { collect } from './lib/collect'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getPackages } from './lib/get-packages'; +import { getVersionNumber } from './lib/version'; +import { writeJson } from './lib/write-json'; + +export const run = async (program: CommanderStatic) => { + program + .option(OPTION_SEMVER_RANGE.spec, OPTION_SEMVER_RANGE.description) + .option(OPTION_SOURCES.spec, OPTION_SOURCES.description, collect) + .option(OPTIONS_PROD.spec, OPTIONS_PROD.description) + .option(OPTIONS_DEV.spec, OPTIONS_DEV.description) + .option(OPTIONS_PEER.spec, OPTIONS_PEER.description) + .parse(process.argv); + + const semverRange: string = + program.semverRange || OPTION_SEMVER_RANGE.default; + + const dependencyTypes = getDependencyTypes(program); + const pkgs = await getPackages(program); + + _(pkgs).each((pkg) => + _(dependencyTypes) + .map((property) => pkg.data[property]) + .filter(Boolean) + .each((dependencies) => { + _(dependencies).each((version, name) => { + const versionNumber = getVersionNumber(version); + if (version !== '*' && semver.validRange(version)) { + if (semverRange === RANGE_ANY) { + dependencies[name] = '*'; + } else if (semverRange === RANGE_LOOSE) { + dependencies[name] = + semver.major(versionNumber) === 0 + ? `${semver.major(versionNumber)}.${semver.minor( + versionNumber + )}.x` + : `${semver.major(versionNumber)}.x.x`; + } else { + dependencies[name] = `${semverRange}${versionNumber}`; + } + } + }); + }) + ); + + await Promise.all(pkgs.map(({ data, path }) => writeJson(path, data))); + + _.each(pkgs, (pkg) => { + console.log(chalk.blue(`./${relative('.', pkg.path)}`)); + }); +}; diff --git a/src/syncpack.ts b/src/syncpack.ts new file mode 100644 index 00000000..db8fe47d --- /dev/null +++ b/src/syncpack.ts @@ -0,0 +1,21 @@ +import { CommanderStatic } from 'commander'; +import { + FIX_MISMATCHES, + FORMAT, + LIST, + LIST_MISMATCHES, + SET_SEMVER_RANGES, + VERSION +} from './constants'; + +export const run = (program: CommanderStatic) => { + program + .version(VERSION) + .command(FIX_MISMATCHES.command, FIX_MISMATCHES.description) + .command(FORMAT.command, FORMAT.description) + .command(LIST.command, LIST.description, { isDefault: true }) + .command(LIST_MISMATCHES.command, LIST_MISMATCHES.description) + .command(SET_SEMVER_RANGES.command, SET_SEMVER_RANGES.description); + + program.parse(process.argv); +}; diff --git a/tsconfig.json b/tsconfig.json index 76362598..c9f84d62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "declaration": true, "forceConsistentCasingInFileNames": true, - "lib": ["es6"], + "lib": ["es2017", "es6"], "outDir": "./dist", "skipLibCheck": true, "strict": true,