From beee8ad2257e3cbcdf2cb0471f6e70cf855c1516 Mon Sep 17 00:00:00 2001 From: Jamie Mason Date: Sun, 16 Feb 2020 13:39:03 +0000 Subject: [PATCH] refactor(core): huge internal refactor --- .prettierrc | 2 +- README.md | 24 ++- global.d.ts | 1 - jest.config.js | 17 +- src/bin-fix-mismatches.ts | 91 ++++++----- src/bin-format.ts | 68 ++++---- src/bin-list-mismatches.ts | 80 +++++----- src/bin-list.ts | 85 +++++----- src/bin-set-semver-ranges.ts | 106 +++++++------ src/bin.ts | 10 +- .../__snapshots__/fix-mismatches.spec.ts.snap | 22 +++ .../list-mismatches.spec.ts.snap | 15 ++ src/commands/__snapshots__/list.spec.ts.snap | 9 ++ src/commands/fix-mismatches.spec.ts | 22 +++ src/commands/fix-mismatches.ts | 52 +++++++ src/commands/format.spec.ts | 42 +++++ src/commands/format.ts | 59 +++++++ src/commands/lib/get-dependency-types.spec.ts | 13 ++ .../lib/get-dependency-types.ts | 12 +- src/commands/lib/get-highest-version.spec.ts | 54 +++++++ src/commands/lib/get-highest-version.ts | 49 ++++++ src/commands/lib/get-installations.spec.ts | 38 +++++ src/commands/lib/get-installations.ts | 88 +++++++++++ .../lib/get-wrappers.spec.ts} | 45 +++--- src/commands/lib/get-wrappers.ts | 50 ++++++ src/commands/lib/is-semver.spec.ts | 19 +++ src/commands/lib/is-semver.ts | 28 ++++ src/commands/lib/log.ts | 1 + src/commands/lib/write-if-changed.ts | 20 +++ src/commands/list-mismatches.spec.ts | 22 +++ src/commands/list-mismatches.ts | 42 +++++ src/commands/list.spec.ts | 22 +++ src/commands/list.ts | 37 +++++ src/commands/set-semver-ranges.spec.ts | 78 ++++++++++ src/commands/set-semver-ranges.ts | 54 +++++++ src/constants.ts | 107 ++++--------- src/fix-mismatches.spec.ts | 60 ------- src/fix-mismatches.ts | 77 --------- src/format.spec.ts | 108 ------------- src/format.ts | 86 ----------- src/lib/collect.spec.ts | 7 - src/lib/collect.ts | 4 - src/lib/get-dependency-types.spec.ts | 20 --- src/lib/get-indent.ts | 7 - src/lib/get-packages.ts | 41 ----- src/lib/get-versions-by-name.ts | 81 ---------- src/lib/version.spec.ts | 146 ------------------ src/lib/version.ts | 64 -------- src/list-mismatches.spec.ts | 70 --------- src/list-mismatches.ts | 42 ----- src/list.spec.ts | 59 ------- src/list.ts | 46 ------ src/set-semver-ranges.spec.ts | 59 ------- src/set-semver-ranges.ts | 83 ---------- src/syncpack.spec.ts | 27 ---- src/syncpack.ts | 21 --- src/typings.ts | 36 ----- test/fixtures/any.json | 37 ----- test/fixtures/exact.json | 38 ----- test/fixtures/gt.json | 37 ----- test/fixtures/gte.json | 37 ----- test/fixtures/loose.json | 37 ----- test/fixtures/lt.json | 37 ----- test/fixtures/lte.json | 37 ----- test/fixtures/minor.json | 37 ----- test/fixtures/patch.json | 37 ----- test/fixtures/untidy.json | 51 ------ test/helpers.ts | 39 ----- test/mock.ts | 19 +++ test/setup.ts | 3 - tsconfig.json | 1 + tslint.json | 2 + 72 files changed, 1172 insertions(+), 1905 deletions(-) delete mode 100644 global.d.ts create mode 100644 src/commands/__snapshots__/fix-mismatches.spec.ts.snap create mode 100644 src/commands/__snapshots__/list-mismatches.spec.ts.snap create mode 100644 src/commands/__snapshots__/list.spec.ts.snap create mode 100644 src/commands/fix-mismatches.spec.ts create mode 100644 src/commands/fix-mismatches.ts create mode 100644 src/commands/format.spec.ts create mode 100644 src/commands/format.ts create mode 100644 src/commands/lib/get-dependency-types.spec.ts rename src/{ => commands}/lib/get-dependency-types.ts (54%) create mode 100644 src/commands/lib/get-highest-version.spec.ts create mode 100644 src/commands/lib/get-highest-version.ts create mode 100644 src/commands/lib/get-installations.spec.ts create mode 100644 src/commands/lib/get-installations.ts rename src/{lib/get-packages.spec.ts => commands/lib/get-wrappers.spec.ts} (54%) create mode 100644 src/commands/lib/get-wrappers.ts create mode 100644 src/commands/lib/is-semver.spec.ts create mode 100644 src/commands/lib/is-semver.ts create mode 100644 src/commands/lib/log.ts create mode 100644 src/commands/lib/write-if-changed.ts create mode 100644 src/commands/list-mismatches.spec.ts create mode 100644 src/commands/list-mismatches.ts create mode 100644 src/commands/list.spec.ts create mode 100644 src/commands/list.ts create mode 100644 src/commands/set-semver-ranges.spec.ts create mode 100644 src/commands/set-semver-ranges.ts delete mode 100644 src/fix-mismatches.spec.ts delete mode 100644 src/fix-mismatches.ts delete mode 100644 src/format.spec.ts delete mode 100644 src/format.ts delete mode 100644 src/lib/collect.spec.ts delete mode 100644 src/lib/collect.ts delete mode 100644 src/lib/get-dependency-types.spec.ts delete mode 100644 src/lib/get-indent.ts delete mode 100644 src/lib/get-packages.ts delete mode 100644 src/lib/get-versions-by-name.ts delete mode 100644 src/lib/version.spec.ts delete mode 100644 src/lib/version.ts delete mode 100644 src/list-mismatches.spec.ts delete mode 100644 src/list-mismatches.ts delete mode 100644 src/list.spec.ts delete mode 100644 src/list.ts delete mode 100644 src/set-semver-ranges.spec.ts delete mode 100644 src/set-semver-ranges.ts delete mode 100644 src/syncpack.spec.ts delete mode 100644 src/syncpack.ts delete mode 100644 src/typings.ts delete mode 100644 test/fixtures/any.json delete mode 100644 test/fixtures/exact.json delete mode 100644 test/fixtures/gt.json delete mode 100644 test/fixtures/gte.json delete mode 100644 test/fixtures/loose.json delete mode 100644 test/fixtures/lt.json delete mode 100644 test/fixtures/lte.json delete mode 100644 test/fixtures/minor.json delete mode 100644 test/fixtures/patch.json delete mode 100644 test/fixtures/untidy.json delete mode 100644 test/helpers.ts create mode 100644 test/mock.ts delete mode 100644 test/setup.ts diff --git a/.prettierrc b/.prettierrc index f13c4e0c..0c2cb34d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { "arrowParens": "always", - "printWidth": 80, + "printWidth": 120, "proseWrap": "always", "singleQuote": true, "trailingComma": "all" diff --git a/README.md b/README.md index 05998ebf..e36df586 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Ensure that multiple packages requiring the same dependency define the same vers -p, --prod include dependencies -d, --dev include devDependencies -P, --peer include peerDependencies + -f, --filter [pattern] regex for dependency filter -i, --indent [value] override indentation. defaults to " " -h, --help output usage information @@ -54,11 +55,11 @@ syncpack fix-mismatches syncpack fix-mismatches --source "apps/*/package.json" # multiple globs can be provided like this syncpack fix-mismatches --source "apps/*/package.json" --source "core/*/package.json" -# uses packages that pass the regex defined by --filter when provided -syncpack fix-mismatches --filter "^package_name$" -# only fix "devDependencies" +# uses dependencies regular expression defined by --filter when provided +syncpack fix-mismatches --filter "typescript|tslint" +# only inspect "devDependencies" syncpack fix-mismatches --dev -# only fix "devDependencies" and "peerDependencies" +# only inspect "devDependencies" and "peerDependencies" syncpack fix-mismatches --dev --peer # indent package.json with 4 spaces instead of 2 syncpack fix-mismatches --indent " " @@ -106,6 +107,7 @@ List all dependencies required by your packages. -p, --prod include dependencies -d, --dev include devDependencies -P, --peer include peerDependencies + -f, --filter [pattern] regex for dependency filter -h, --help output usage information @@ -120,6 +122,8 @@ syncpack list syncpack list --source "apps/*/package.json" # multiple globs can be provided like this syncpack list --source "apps/*/package.json" --source "core/*/package.json" +# uses dependencies regular expression defined by --filter when provided +syncpack list --filter "typescript|tslint" # only inspect "devDependencies" syncpack list --dev # only inspect "devDependencies" and "peerDependencies" @@ -139,6 +143,7 @@ List dependencies which are required by multiple packages, where the version is -p, --prod include dependencies -d, --dev include devDependencies -P, --peer include peerDependencies + -f, --filter [pattern] regex for dependency filter -h, --help output usage information @@ -153,9 +158,11 @@ syncpack list-mismatches syncpack list-mismatches --source "apps/*/package.json" # multiple globs can be provided like this syncpack list-mismatches --source "apps/*/package.json" --source "core/*/package.json" -# only list "devDependencies" +# uses dependencies regular expression defined by --filter when provided +syncpack list-mismatches --filter "typescript|tslint" +# only inspect "devDependencies" syncpack list-mismatches --dev -# only list "devDependencies" and "peerDependencies" +# only inspect "devDependencies" and "peerDependencies" syncpack list-mismatches --dev --peer ``` @@ -168,12 +175,13 @@ Ensure dependency versions used within `"dependencies"`, `"devDependencies"`, an
Options - -r, --semver-range <, <=, "", ~, ^, >=, >, or *. defaults to "" -s, --source [pattern] glob pattern for package.json files to read from -p, --prod include dependencies -d, --dev include devDependencies -P, --peer include peerDependencies + -f, --filter [pattern] regex for dependency filter -i, --indent [value] override indentation. defaults to " " + -r, --semver-range see supported ranges below. defaults to "" -h, --help output usage information
@@ -188,6 +196,8 @@ syncpack set-semver-ranges syncpack set-semver-ranges --source "apps/*/package.json" # multiple globs can be provided like this syncpack set-semver-ranges --source "apps/*/package.json" --source "core/*/package.json" +# uses dependencies regular expression defined by --filter when provided +syncpack set-semver-ranges --filter "typescript|tslint" # use ~ range instead of default "" syncpack set-semver-ranges --semver-range ~ # set ~ range in "devDependencies" diff --git a/global.d.ts b/global.d.ts deleted file mode 100644 index c5ba6121..00000000 --- a/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -import 'expect-more-jest'; diff --git a/jest.config.js b/jest.config.js index ddddf20a..a4df4962 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,19 +1,18 @@ module.exports = { collectCoverage: true, - collectCoverageFrom: ['src/**/*.ts', '!src/**/typings.ts', '!src/**/bin*.ts'], + collectCoverageFrom: ['src/**/*.ts', '!src/**/bin*.ts'], coverageReporters: ['html', 'lcov'], coverageThreshold: { global: { - branches: 95, - functions: 95, - lines: 95, - statements: 95 - } + branches: 82, + functions: 72, + lines: 81, + statements: 77, + }, }, moduleFileExtensions: ['ts', 'tsx', 'js'], - setupFilesAfterEnv: ['/test/setup.ts'], testMatch: ['/src/**/*.spec.(ts|tsx|js)'], transform: { - '^.+\\.tsx?$': 'ts-jest' - } + '^.+\\.tsx?$': 'ts-jest', + }, }; diff --git a/src/bin-fix-mismatches.ts b/src/bin-fix-mismatches.ts index d9100de9..e512b0d2 100644 --- a/src/bin-fix-mismatches.ts +++ b/src/bin-fix-mismatches.ts @@ -2,62 +2,61 @@ import chalk from 'chalk'; import program = require('commander'); -import { run } from './fix-mismatches'; +import { fixMismatchesToDisk } from './commands/fix-mismatches'; +import { option } from './constants'; program.description( ` Ensure that multiple packages requiring the same dependency define the same version, so that every package requires eg. react@16.4.2, instead of a - combination of react@16.4.2, react@0.15.9, and react@16.0.0.`.replace( - /^\n/, - '', - ), + combination of react@16.4.2, react@0.15.9, and react@16.0.0.`.replace(/^\n/, ''), ); program.on('--help', () => { - console.log(''); - console.log(`Examples: - ${chalk.grey('# uses defaults for resolving packages')} + console.log(chalk` +Resolving Packages: + 1. If {yellow --source} globs are provided, use those. + 2. If using Yarn Workspaces, read {yellow workspaces} from {yellow package.json}. + 3. If using Lerna, read {yellow packages} from {yellow lerna.json}. + 4. Default to {yellow "package.json"} and {yellow "packages/*/package.json"}. + +Examples: + {dim # uses defaults for resolving packages} syncpack fix-mismatches - ${chalk.grey('# uses packages defined by --source when provided')} - syncpack fix-mismatches --source ${chalk.yellow('"apps/*/package.json"')} - ${chalk.grey( - '# uses dependencies regular expression defined by --filter when provided', - )} - syncpack fix-mismatches --filter ${chalk.yellow('"typescript|tslint"')} - ${chalk.grey('# multiple globs can be provided like this')} - syncpack fix-mismatches --source ${chalk.yellow( - '"apps/*/package.json"', - )} --source ${chalk.yellow('"core/*/package.json"')} - ${chalk.grey('# only fix "devDependencies"')} + {dim # uses packages defined by --source when provided} + syncpack fix-mismatches --source {yellow "apps/*/package.json"} + {dim # multiple globs can be provided like this} + syncpack fix-mismatches --source {yellow "apps/*/package.json"} --source {yellow "core/*/package.json"} + {dim # uses dependencies regular expression defined by --filter when provided} + syncpack fix-mismatches --filter {yellow "typescript|tslint"} + {dim # only inspect "devDependencies"} syncpack fix-mismatches --dev - ${chalk.grey('# only fix "devDependencies" and "peerDependencies"')} + {dim # only inspect "devDependencies" and "peerDependencies"} syncpack fix-mismatches --dev --peer - ${chalk.grey('# indent package.json with 4 spaces instead of 2')} - syncpack fix-mismatches --indent ${chalk.yellow('" "')} - `); - console.log(`Resolving Packages: - 1. If ${chalk.yellow(`--source`)} globs are provided, use those. - 2. If using Yarn Workspaces, read ${chalk.yellow( - `workspaces`, - )} from ${chalk.yellow(`package.json`)}. - 3. If using Lerna, read ${chalk.yellow(`packages`)} from ${chalk.yellow( - `lerna.json`, - )}. - 4. Default to ${chalk.yellow(`"package.json"`)} and ${chalk.yellow( - `"packages/*/package.json"`, - )}. - `); - console.log(`Reference: - globs ${chalk.blue.underline( - 'https://github.com/isaacs/node-glob#glob-primer', - )} - lerna.json ${chalk.blue.underline( - 'https://github.com/lerna/lerna#lernajson', - )} - Yarn Workspaces ${chalk.blue.underline( - 'https://yarnpkg.com/lang/en/docs/workspaces', - )}`); + {dim # indent package.json with 4 spaces instead of 2} + syncpack fix-mismatches --indent {yellow " "} + +Reference: + globs {blue.underline https://github.com/isaacs/node-glob#glob-primer} + lerna.json {blue.underline https://github.com/lerna/lerna#lernajson} + Yarn Workspaces {blue.underline https://yarnpkg.com/lang/en/docs/workspaces} +`); }); -run(program); +program + .option(...option.source) + .option(...option.prod) + .option(...option.dev) + .option(...option.peer) + .option(...option.filter) + .option(...option.indent) + .parse(process.argv); + +fixMismatchesToDisk({ + dev: Boolean(program.dev), + filter: new RegExp(program.filter ? program.filter : '.'), + indent: program.indent ? program.indent : ' ', + peer: Boolean(program.peer), + prod: Boolean(program.prod), + source: Array.isArray(program.source) ? program.source : [], +}); diff --git a/src/bin-format.ts b/src/bin-format.ts index 63a0f255..75b10c86 100644 --- a/src/bin-format.ts +++ b/src/bin-format.ts @@ -2,7 +2,8 @@ import chalk from 'chalk'; import program = require('commander'); -import { run } from './format'; +import { formatToDisk } from './commands/format'; +import { option } from './constants'; program.description( ` @@ -13,41 +14,36 @@ program.description( ); program.on('--help', () => { - console.log(''); - console.log(`Examples: - ${chalk.grey('# uses defaults for resolving packages')} + console.log(chalk` +Examples: + {dim # uses defaults for resolving packages} syncpack format - ${chalk.grey('# uses packages defined by --source when provided')} - syncpack format --source ${chalk.yellow('"apps/*/package.json"')} - ${chalk.grey('# multiple globs can be provided like this')} - syncpack format --source ${chalk.yellow( - '"apps/*/package.json"', - )} --source ${chalk.yellow('"core/*/package.json"')} - ${chalk.grey('# indent package.json with 4 spaces instead of 2')} - syncpack format --indent ${chalk.yellow('" "')} - `); - console.log(`Resolving Packages: - 1. If ${chalk.yellow(`--source`)} globs are provided, use those. - 2. If using Yarn Workspaces, read ${chalk.yellow( - `workspaces`, - )} from ${chalk.yellow(`package.json`)}. - 3. If using Lerna, read ${chalk.yellow(`packages`)} from ${chalk.yellow( - `lerna.json`, - )}. - 4. Default to ${chalk.yellow(`"package.json"`)} and ${chalk.yellow( - `"packages/*/package.json"`, - )}. - `); - console.log(`Reference: - globs ${chalk.blue.underline( - 'https://github.com/isaacs/node-glob#glob-primer', - )} - lerna.json ${chalk.blue.underline( - 'https://github.com/lerna/lerna#lernajson', - )} - Yarn Workspaces ${chalk.blue.underline( - 'https://yarnpkg.com/lang/en/docs/workspaces', - )}`); + {dim # uses packages defined by --source when provided} + syncpack format --source {yellow "apps/*/package.json"} + {dim # multiple globs can be provided like this} + syncpack format --source {yellow "apps/*/package.json"} --source {yellow "core/*/package.json"} + {dim # indent package.json with 4 spaces instead of 2} + syncpack format --indent {yellow " "} + +Resolving Packages: + 1. If {yellow --source} globs are provided, use those. + 2. If using Yarn Workspaces, read {yellow workspaces} from {yellow package.json}. + 3. If using Lerna, read {yellow packages} from {yellow lerna.json}. + 4. Default to {yellow "package.json"} and {yellow "packages/*/package.json"}. + +Reference: + globs {blue.underline https://github.com/isaacs/node-glob#glob-primer} + lerna.json {blue.underline https://github.com/lerna/lerna#lernajson} + Yarn Workspaces {blue.underline https://yarnpkg.com/lang/en/docs/workspaces} +`); }); -run(program); +program + .option(...option.source) + .option(...option.indent) + .parse(process.argv); + +formatToDisk({ + indent: program.indent ? program.indent : ' ', + source: Array.isArray(program.source) ? program.source : [], +}); diff --git a/src/bin-list-mismatches.ts b/src/bin-list-mismatches.ts index 7b7def39..25099e17 100644 --- a/src/bin-list-mismatches.ts +++ b/src/bin-list-mismatches.ts @@ -2,7 +2,8 @@ import chalk from 'chalk'; import program = require('commander'); -import { run } from './list-mismatches'; +import { listMismatchesFromDisk } from './commands/list-mismatches'; +import { option } from './constants'; program.description( ` @@ -11,47 +12,46 @@ program.description( ); program.on('--help', () => { - console.log(''); - console.log(`Examples: - ${chalk.grey('# uses defaults for resolving packages')} + console.log(chalk` +Examples: + {dim # uses defaults for resolving packages} syncpack list-mismatches - ${chalk.grey('# uses packages defined by --source when provided')} - syncpack list-mismatches --source ${chalk.yellow('"apps/*/package.json"')} - ${chalk.grey('# multiple globs can be provided like this')} - syncpack list-mismatches --source ${chalk.yellow( - '"apps/*/package.json"', - )} --source ${chalk.yellow('"core/*/package.json"')} - ${chalk.grey( - '# uses dependencies regular expression defined by --filter when provided', - )} - syncpack list-mismatches --filter ${chalk.yellow('"typescript|tslint"')} - ${chalk.grey('# only list "devDependencies"')} + {dim # uses packages defined by --source when provided} + syncpack list-mismatches --source {yellow "apps/*/package.json"} + {dim # multiple globs can be provided like this} + syncpack list-mismatches --source {yellow "apps/*/package.json"} --source {yellow "core/*/package.json"} + {dim # uses dependencies regular expression defined by --filter when provided} + syncpack list-mismatches --filter {yellow "typescript|tslint"} + {dim # only inspect "devDependencies"} syncpack list-mismatches --dev - ${chalk.grey('# only list "devDependencies" and "peerDependencies"')} + {dim # only inspect "devDependencies" and "peerDependencies"} syncpack list-mismatches --dev --peer - `); - console.log(`Resolving Packages: - 1. If ${chalk.yellow(`--source`)} globs are provided, use those. - 2. If using Yarn Workspaces, read ${chalk.yellow( - `workspaces`, - )} from ${chalk.yellow(`package.json`)}. - 3. If using Lerna, read ${chalk.yellow(`packages`)} from ${chalk.yellow( - `lerna.json`, - )}. - 4. Default to ${chalk.yellow(`"package.json"`)} and ${chalk.yellow( - `"packages/*/package.json"`, - )}. - `); - console.log(`Reference: - globs ${chalk.blue.underline( - 'https://github.com/isaacs/node-glob#glob-primer', - )} - lerna.json ${chalk.blue.underline( - 'https://github.com/lerna/lerna#lernajson', - )} - Yarn Workspaces ${chalk.blue.underline( - 'https://yarnpkg.com/lang/en/docs/workspaces', - )}`); + +Resolving Packages: + 1. If {yellow --source} globs are provided, use those. + 2. If using Yarn Workspaces, read {yellow workspaces} from {yellow package.json}. + 3. If using Lerna, read {yellow packages} from {yellow lerna.json}. + 4. Default to {yellow "package.json"} and {yellow "packages/*/package.json"}. + +Reference: + globs {blue.underline https://github.com/isaacs/node-glob#glob-primer} + lerna.json {blue.underline https://github.com/lerna/lerna#lernajson} + Yarn Workspaces {blue.underline https://yarnpkg.com/lang/en/docs/workspaces} +`); }); -run(program); +program + .option(...option.source) + .option(...option.prod) + .option(...option.dev) + .option(...option.peer) + .option(...option.filter) + .parse(process.argv); + +listMismatchesFromDisk({ + dev: Boolean(program.dev), + filter: new RegExp(program.filter ? program.filter : '.'), + peer: Boolean(program.peer), + prod: Boolean(program.prod), + source: Array.isArray(program.source) ? program.source : [], +}); diff --git a/src/bin-list.ts b/src/bin-list.ts index c181d42d..6656c57c 100644 --- a/src/bin-list.ts +++ b/src/bin-list.ts @@ -2,55 +2,52 @@ import chalk from 'chalk'; import program = require('commander'); -import { run } from './list'; +import { listFromDisk } from './commands/list'; +import { option } from './constants'; -program.description( - ` - List all dependencies required by your packages.`.replace(/^\n/, ''), -); +program.description(' List all dependencies required by your packages.'); program.on('--help', () => { - console.log(''); - console.log(`Examples: - ${chalk.grey('# uses defaults for resolving packages')} + console.log(chalk` +Examples: + {dim # uses defaults for resolving packages} syncpack list - ${chalk.grey('# uses packages defined by --source when provided')} - syncpack list --source ${chalk.yellow('"apps/*/package.json"')} - ${chalk.grey( - '# uses dependencies regular expression defined by --filter when provided', - )} - syncpack list --filter ${chalk.yellow('"typescript|tslint"')} - ${chalk.grey('# multiple globs can be provided like this')} - syncpack list --source ${chalk.yellow( - '"apps/*/package.json"', - )} --source ${chalk.yellow('"core/*/package.json"')} - ${chalk.grey('# only inspect "devDependencies"')} + {dim # uses packages defined by --source when provided} + syncpack list --source {yellow "apps/*/package.json"} + {dim # multiple globs can be provided like this} + syncpack list --source {yellow "apps/*/package.json"} --source {yellow "core/*/package.json"} + {dim # uses dependencies regular expression defined by --filter when provided} + syncpack list --filter {yellow "typescript|tslint"} + {dim # only inspect "devDependencies"} syncpack list --dev - ${chalk.grey('# only inspect "devDependencies" and "peerDependencies"')} + {dim # only inspect "devDependencies" and "peerDependencies"} syncpack list --dev --peer - `); - console.log(`Resolving Packages: - 1. If ${chalk.yellow(`--source`)} globs are provided, use those. - 2. If using Yarn Workspaces, read ${chalk.yellow( - `workspaces`, - )} from ${chalk.yellow(`package.json`)}. - 3. If using Lerna, read ${chalk.yellow(`packages`)} from ${chalk.yellow( - `lerna.json`, - )}. - 4. Default to ${chalk.yellow(`"package.json"`)} and ${chalk.yellow( - `"packages/*/package.json"`, - )}. - `); - console.log(`Reference: - globs ${chalk.blue.underline( - 'https://github.com/isaacs/node-glob#glob-primer', - )} - lerna.json ${chalk.blue.underline( - 'https://github.com/lerna/lerna#lernajson', - )} - Yarn Workspaces ${chalk.blue.underline( - 'https://yarnpkg.com/lang/en/docs/workspaces', - )}`); + +Resolving Packages: + 1. If {yellow --source} globs are provided, use those. + 2. If using Yarn Workspaces, read {yellow workspaces} from {yellow package.json}. + 3. If using Lerna, read {yellow packages} from {yellow lerna.json}. + 4. Default to {yellow "package.json"} and {yellow "packages/*/package.json"}. + +Reference: + globs {blue.underline https://github.com/isaacs/node-glob#glob-primer} + lerna.json {blue.underline https://github.com/lerna/lerna#lernajson} + Yarn Workspaces {blue.underline https://yarnpkg.com/lang/en/docs/workspaces} +`); }); -run(program); +program + .option(...option.source) + .option(...option.prod) + .option(...option.dev) + .option(...option.peer) + .option(...option.filter) + .parse(process.argv); + +listFromDisk({ + dev: Boolean(program.dev), + filter: new RegExp(program.filter ? program.filter : '.'), + peer: Boolean(program.peer), + prod: Boolean(program.prod), + source: Array.isArray(program.source) ? program.source : [], +}); diff --git a/src/bin-set-semver-ranges.ts b/src/bin-set-semver-ranges.ts index 14e049a9..1d73481d 100644 --- a/src/bin-set-semver-ranges.ts +++ b/src/bin-set-semver-ranges.ts @@ -2,7 +2,8 @@ import chalk from 'chalk'; import program = require('commander'); -import { run } from './set-semver-ranges'; +import { setSemverRangesToDisk } from './commands/set-semver-ranges'; +import { option } from './constants'; program.description( ` @@ -11,57 +12,64 @@ program.description( ); program.on('--help', () => { - console.log(''); - console.log(`Examples: - ${chalk.grey('# uses defaults for resolving packages')} + console.log(chalk` +Examples: + {dim # uses defaults for resolving packages} syncpack set-semver-ranges - ${chalk.grey('# uses packages defined by --source when provided')} - syncpack set-semver-ranges --source ${chalk.yellow('"apps/*/package.json"')} - ${chalk.grey('# multiple globs can be provided like this')} - syncpack set-semver-ranges --source ${chalk.yellow( - '"apps/*/package.json"', - )} --source ${chalk.yellow('"core/*/package.json"')} - ${chalk.grey('# use ~ range instead of default ""')} + {dim # uses packages defined by --source when provided} + syncpack set-semver-ranges --source {yellow "apps/*/package.json"} + {dim # multiple globs can be provided like this} + syncpack set-semver-ranges --source {yellow "apps/*/package.json"} --source {yellow "core/*/package.json"} + {dim # uses dependencies regular expression defined by --filter when provided} + syncpack set-semver-ranges --filter {yellow "typescript|tslint"} + {dim # use ~ range instead of default ""} syncpack set-semver-ranges --semver-range ~ - ${chalk.grey('# set ~ range in "devDependencies"')} + {dim # set ~ range in "devDependencies"} syncpack set-semver-ranges --dev --semver-range ~ - ${chalk.grey('# set ~ range in "devDependencies" and "peerDependencies"')} + {dim # set ~ range in "devDependencies" and "peerDependencies"} syncpack set-semver-ranges --dev --peer --semver-range ~ - ${chalk.grey('# indent package.json with 4 spaces instead of 2')} - syncpack set-semver-ranges --indent ${chalk.yellow('" "')} - `); - console.log(`Supported Ranges: - < ${chalk.grey('<1.4.2')} - <= ${chalk.grey('<=1.4.2')} - "" ${chalk.grey('1.4.2')} - ~ ${chalk.grey('~1.4.2')} - ^ ${chalk.grey('^1.4.2')} - >= ${chalk.grey('>=1.4.2')} - > ${chalk.grey('>1.4.2')} - * ${chalk.grey('*')} - `); - console.log(`Resolving Packages: - 1. If ${chalk.yellow(`--source`)} globs are provided, use those. - 2. If using Yarn Workspaces, read ${chalk.yellow( - `workspaces`, - )} from ${chalk.yellow(`package.json`)}. - 3. If using Lerna, read ${chalk.yellow(`packages`)} from ${chalk.yellow( - `lerna.json`, - )}. - 4. Default to ${chalk.yellow(`"package.json"`)} and ${chalk.yellow( - `"packages/*/package.json"`, - )}. - `); - console.log(`Reference: - globs ${chalk.blue.underline( - 'https://github.com/isaacs/node-glob#glob-primer', - )} - lerna.json ${chalk.blue.underline( - 'https://github.com/lerna/lerna#lernajson', - )} - Yarn Workspaces ${chalk.blue.underline( - 'https://yarnpkg.com/lang/en/docs/workspaces', - )}`); + {dim # indent package.json with 4 spaces instead of 2} + syncpack set-semver-ranges --indent {yellow " "} + +Supported Ranges: + < {dim <1.4.2} + <= {dim <=1.4.2} + "" {dim 1.4.2} + ~ {dim ~1.4.2} + ^ {dim ^1.4.2} + >= {dim >=1.4.2} + > {dim >1.4.2} + * {dim *} + +Resolving Packages: + 1. If {yellow --source} globs are provided, use those. + 2. If using Yarn Workspaces, read {yellow workspaces} from {yellow package.json}. + 3. If using Lerna, read {yellow packages} from {yellow lerna.json}. + 4. Default to {yellow "package.json"} and {yellow "packages/*/package.json"}. + +Reference: + globs {blue.underline https://github.com/isaacs/node-glob#glob-primer} + lerna.json {blue.underline https://github.com/lerna/lerna#lernajson} + Yarn Workspaces {blue.underline https://yarnpkg.com/lang/en/docs/workspaces} +`); }); -run(program); +program + .option(...option.source) + .option(...option.prod) + .option(...option.dev) + .option(...option.peer) + .option(...option.filter) + .option(...option.indent) + .option(...option.semverRange) + .parse(process.argv); + +setSemverRangesToDisk({ + dev: Boolean(program.dev), + filter: new RegExp(program.filter ? program.filter : '.'), + indent: program.indent ? program.indent : ' ', + peer: Boolean(program.peer), + prod: Boolean(program.prod), + semverRange: program.semverRange ? program.semverRange : '', + source: Array.isArray(program.source) ? program.source : [], +}); diff --git a/src/bin.ts b/src/bin.ts index b0417dd2..bf2d36a9 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,6 +1,12 @@ #!/usr/bin/env node import program = require('commander'); -import { run } from './syncpack'; -run(program); +program + .version(require('../package.json').version) + .command('fix-mismatches', 'set dependencies used with different versions to the same version') + .command('format', 'sort and shorten properties according to a convention') + .command('list', 'list every dependency used in your packages', { isDefault: true }) + .command('list-mismatches', 'list every dependency used with different versions in your packages') + .command('set-semver-ranges', 'set semver ranges to the given format') + .parse(process.argv); diff --git a/src/commands/__snapshots__/fix-mismatches.spec.ts.snap b/src/commands/__snapshots__/fix-mismatches.spec.ts.snap new file mode 100644 index 00000000..5215d374 --- /dev/null +++ b/src/commands/__snapshots__/fix-mismatches.spec.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fixMismatches sets all dependencies installed with different versions to the highest version 1`] = ` +Array [ + Object { + "contents": Object { + "dependencies": Object { + "foo": "0.2.0", + }, + }, + "filePath": "/a/package.json", + }, + Object { + "contents": Object { + "dependencies": Object { + "foo": "0.2.0", + }, + }, + "filePath": "/b/package.json", + }, +] +`; diff --git a/src/commands/__snapshots__/list-mismatches.spec.ts.snap b/src/commands/__snapshots__/list-mismatches.spec.ts.snap new file mode 100644 index 00000000..928b1677 --- /dev/null +++ b/src/commands/__snapshots__/list-mismatches.spec.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`listMismatches outputs all dependencies installed with different versions 1`] = ` +Array [ + Array [ + "✕ foo", + ], + Array [ + "- 0.1.0 in dependencies of undefined", + ], + Array [ + "- 0.2.0 in dependencies of undefined", + ], +] +`; diff --git a/src/commands/__snapshots__/list.spec.ts.snap b/src/commands/__snapshots__/list.spec.ts.snap new file mode 100644 index 00000000..9576a237 --- /dev/null +++ b/src/commands/__snapshots__/list.spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`list outputs all dependencies 1`] = ` +Array [ + Array [ + "✕ foo 0.1.0, 0.2.0", + ], +] +`; diff --git a/src/commands/fix-mismatches.spec.ts b/src/commands/fix-mismatches.spec.ts new file mode 100644 index 00000000..91337e54 --- /dev/null +++ b/src/commands/fix-mismatches.spec.ts @@ -0,0 +1,22 @@ +import * as mock from '../../test/mock'; + +describe('fixMismatches', () => { + let fixMismatches: any; + let log: any; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + beforeEach(() => { + jest.mock('./lib/log', () => ({ log: jest.fn() })); + fixMismatches = require('./fix-mismatches').fixMismatches; + log = require('./lib/log').log; + }); + + it('sets all dependencies installed with different versions to the highest version', () => { + const wrappers = [mock.wrapper('a', ['foo@0.1.0']), mock.wrapper('b', ['foo@0.2.0'])]; + fixMismatches(['dependencies'], /./, wrappers); + expect(wrappers).toMatchSnapshot(); + }); +}); diff --git a/src/commands/fix-mismatches.ts b/src/commands/fix-mismatches.ts new file mode 100644 index 00000000..57af4ea6 --- /dev/null +++ b/src/commands/fix-mismatches.ts @@ -0,0 +1,52 @@ +import chalk from 'chalk'; +import { writeFileSync } from 'fs-extra'; +import { EOL } from 'os'; +import { relative } from 'path'; +import { DependencyType } from '../constants'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getHighestVersion } from './lib/get-highest-version'; +import { getMismatchedDependencies } from './lib/get-installations'; +import { getWrappers, SourceWrapper } from './lib/get-wrappers'; +import { log } from './lib/log'; + +interface Options { + dev: boolean; + filter: RegExp; + indent: string; + peer: boolean; + prod: boolean; + source: string[]; +} + +export const fixMismatches = (dependencyTypes: DependencyType[], filter: RegExp, wrappers: SourceWrapper[]) => { + const iterator = getMismatchedDependencies(dependencyTypes, wrappers); + const mismatches = Array.from(iterator).filter(({ name }) => name.search(filter) !== -1); + + mismatches.forEach((installedPackage) => { + const newest = getHighestVersion(installedPackage.installations.map((installation) => installation.version)); + installedPackage.installations.forEach(({ type, name, source }) => { + source.contents[type][name] = newest; + }); + }); +}; + +export const fixMismatchesToDisk = ({ dev, filter, indent, peer, prod, source }: Options) => { + const toJson = (wrapper: SourceWrapper) => `${JSON.stringify(wrapper.contents, null, indent)}${EOL}`; + const dependencyTypes = getDependencyTypes({ dev, peer, prod }); + const wrappers = getWrappers({ source }); + const allBefore = wrappers.map(toJson); + + fixMismatches(dependencyTypes, filter, wrappers); + + wrappers.forEach((wrapper, i) => { + const shortPath = relative(process.cwd(), wrapper.filePath); + const before = allBefore[i]; + const after = toJson(wrapper); + if (before !== after) { + writeFileSync(wrapper.filePath, after); + log(chalk.green('✓'), shortPath); + } else { + log(chalk.dim('-', shortPath)); + } + }); +}; diff --git a/src/commands/format.spec.ts b/src/commands/format.spec.ts new file mode 100644 index 00000000..0a1772e3 --- /dev/null +++ b/src/commands/format.spec.ts @@ -0,0 +1,42 @@ +import { format } from './format'; +import { Source } from './lib/get-wrappers'; + +const createWrapper = (contents: Source) => ({ contents, filePath: '' }); + +describe('format', () => { + it('sorts Array properties alphabetically by value', () => { + const wrapper = createWrapper({ keywords: ['B', 'A'] }); + expect(format(wrapper, { sortAz: ['keywords'] })).toEqual({ + keywords: ['A', 'B'], + }); + }); + it('sorts Object properties alphabetically by key', () => { + const wrapper = createWrapper({ scripts: { B: '', A: '' } }); + expect(format(wrapper, { sortAz: ['scripts'] })).toEqual({ + scripts: { A: '', B: '' }, + }); + }); + it('sorts named properties first, then the rest alphabetically', () => { + const wrapper = createWrapper({ A: '', C: '', F: '', B: '', D: '', E: '' }); + expect(format(wrapper, { sortFirst: ['D', 'E', 'f'] })).toEqual({ + D: '', + E: '', + F: '', + A: '', + B: '', + C: '', + }); + }); + it('uses shorthand format for "bugs"', () => { + const wrapper = createWrapper({ bugs: { url: 'https://github.com/User/repo/issues' } }); + expect(format(wrapper)).toEqual({ bugs: 'https://github.com/User/repo/issues' }); + }); + it('uses shorthand format for "repository"', () => { + const wrapper = createWrapper({ repository: { url: 'git://gitlab.com/User/repo', type: 'git' } }); + expect(format(wrapper)).toEqual({ repository: 'git://gitlab.com/User/repo' }); + }); + it('uses github shorthand format for "repository"', () => { + const wrapper = createWrapper({ repository: { url: 'git://github.com/User/repo', type: 'git' } }); + expect(format(wrapper)).toEqual({ repository: 'User/repo' }); + }); +}); diff --git a/src/commands/format.ts b/src/commands/format.ts new file mode 100644 index 00000000..3b107244 --- /dev/null +++ b/src/commands/format.ts @@ -0,0 +1,59 @@ +import { SORT_AZ, SORT_FIRST } from '../constants'; +import { getWrappers, SourceWrapper, Source } from './lib/get-wrappers'; +import { writeIfChanged } from './lib/write-if-changed'; + +interface FormatConfig { + sortAz?: string[]; + sortFirst?: string[]; +} + +interface Options { + indent: string; + source: string[]; +} + +const sortObject = (sortedKeys: string[] | Set, obj: any) => { + sortedKeys.forEach((key: string) => { + const value = obj[key]; + delete obj[key]; + obj[key] = value; + }); +}; + +const sortAlphabetically = (value: any) => { + if (Array.isArray(value)) { + value.sort(); + } else if (Object.prototype.toString.call(value) === '[object Object]') { + sortObject(Object.keys(value).sort(), value); + } +}; + +export const format = (wrapper: SourceWrapper, { sortAz = SORT_AZ, sortFirst = SORT_FIRST }: FormatConfig = {}) => { + const { contents } = wrapper; + const sortedKeys = Object.keys(contents).sort(); + const keys = new Set(sortFirst.concat(sortedKeys)); + + if (contents.bugs && typeof contents.bugs === 'object' && contents.bugs.url) { + contents.bugs = contents.bugs.url; + } + + if (contents.repository && typeof contents.repository === 'object' && contents.repository.url) { + if (contents.repository.url.includes('github.com')) { + contents.repository = contents.repository.url.replace(/^.+github\.com\//, ''); + } else { + contents.repository = contents.repository.url; + } + } + + sortAz.forEach((key) => sortAlphabetically(contents[key])); + sortObject(keys, contents); + return contents; +}; + +export const formatToDisk = ({ indent, source }: Options) => { + getWrappers({ source }).forEach((wrapper) => { + writeIfChanged(indent, wrapper, () => { + format(wrapper); + }); + }); +}; diff --git a/src/commands/lib/get-dependency-types.spec.ts b/src/commands/lib/get-dependency-types.spec.ts new file mode 100644 index 00000000..5127298b --- /dev/null +++ b/src/commands/lib/get-dependency-types.spec.ts @@ -0,0 +1,13 @@ +import { getDependencyTypes } from './get-dependency-types'; + +describe('getDependencyTypes', () => { + it('returns all if none are set or true if any are set', () => { + const prod = 'dependencies'; + const dev = 'devDependencies'; + const peer = 'peerDependencies'; + expect(getDependencyTypes({ dev: false, peer: false, prod: false })).toEqual([prod, dev, peer]); + expect(getDependencyTypes({ dev: true, peer: false, prod: false })).toEqual([dev]); + expect(getDependencyTypes({ dev: false, peer: true, prod: false })).toEqual([peer]); + expect(getDependencyTypes({ dev: false, peer: false, prod: true })).toEqual([prod]); + }); +}); diff --git a/src/lib/get-dependency-types.ts b/src/commands/lib/get-dependency-types.ts similarity index 54% rename from src/lib/get-dependency-types.ts rename to src/commands/lib/get-dependency-types.ts index 39b2d60c..ed7525c7 100644 --- a/src/lib/get-dependency-types.ts +++ b/src/commands/lib/get-dependency-types.ts @@ -1,7 +1,13 @@ -import { DEPENDENCY_TYPES } from '../constants'; -import { CommanderApi, IManifestKey } from '../typings'; +import { DEPENDENCY_TYPES } from '../../constants'; +import { DependencyType } from '../../constants'; -export const getDependencyTypes = (program: CommanderApi): IManifestKey[] => +interface Options { + prod: boolean; + dev: boolean; + peer: boolean; +} + +export const getDependencyTypes = (program: Options): DependencyType[] => program.prod || program.dev || program.peer ? DEPENDENCY_TYPES.filter( (type) => diff --git a/src/commands/lib/get-highest-version.spec.ts b/src/commands/lib/get-highest-version.spec.ts new file mode 100644 index 00000000..64a03fbb --- /dev/null +++ b/src/commands/lib/get-highest-version.spec.ts @@ -0,0 +1,54 @@ +import { getHighestVersion } from './get-highest-version'; + +const shuffle = (array: any[]) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}; + +describe('getHighestVersion', () => { + it('returns the newest version from an array of versions', () => { + const a = ['<1.0.0']; + const b = shuffle([...a, '<=1.0.0']); + const c = shuffle([...b, '1.0.0']); + const d = shuffle([...c, '~1.0.0']); + const e = shuffle([...d, '1.x.x']); + const f = shuffle([...e, '^1.0.0']); + const g = shuffle([...f, '>=1.0.0']); + const h = shuffle([...g, '>1.0.0']); + const i = shuffle([...h, '*']); + const j = shuffle([...i, 'http://asdf.com/asdf.tar.gz']); + const k = shuffle([...j, 'file:../foo/bar']); + const l = shuffle([...k, 'latest']); + const m = shuffle([...l, 'git+ssh://git@github.com:npm/cli.git#v1.0.27']); + const n = shuffle([...m, 'git+ssh://git@github.com:npm/cli#semver:^5.0']); + const o = shuffle([...n, 'git+https://isaacs@github.com/npm/cli.git']); + const p = shuffle([...o, 'git://github.com/npm/cli.git#v1.0.27']); + const q = shuffle([...p, 'expressjs/express']); + const r = shuffle([...q, 'mochajs/mocha#4727d357ea']); + const s = shuffle([...r, 'user/repo#feature/branch']); + // valid semver + expect(getHighestVersion(a)).toEqual('<1.0.0'); + expect(getHighestVersion(b)).toEqual('<=1.0.0'); + expect(getHighestVersion(c)).toEqual('1.0.0'); + expect(getHighestVersion(d)).toEqual('~1.0.0'); + expect(getHighestVersion(e)).toEqual('1.x.x'); + expect(getHighestVersion(f)).toEqual('^1.0.0'); + expect(getHighestVersion(g)).toEqual('>=1.0.0'); + expect(getHighestVersion(h)).toEqual('>1.0.0'); + expect(getHighestVersion(i)).toEqual('*'); + // invalid semver + expect(getHighestVersion(j)).toEqual('*'); + expect(getHighestVersion(k)).toEqual('*'); + expect(getHighestVersion(l)).toEqual('*'); + expect(getHighestVersion(m)).toEqual('*'); + expect(getHighestVersion(n)).toEqual('*'); + expect(getHighestVersion(o)).toEqual('*'); + expect(getHighestVersion(p)).toEqual('*'); + expect(getHighestVersion(q)).toEqual('*'); + expect(getHighestVersion(r)).toEqual('*'); + expect(getHighestVersion(s)).toEqual('*'); + }); +}); diff --git a/src/commands/lib/get-highest-version.ts b/src/commands/lib/get-highest-version.ts new file mode 100644 index 00000000..b897aa1b --- /dev/null +++ b/src/commands/lib/get-highest-version.ts @@ -0,0 +1,49 @@ +import { coerce, eq, gt, valid } from 'semver'; +import { + RANGE_ANY, + RANGE_EXACT, + RANGE_GT, + RANGE_GTE, + RANGE_LT, + RANGE_LTE, + RANGE_MINOR, + RANGE_PATCH, +} from '../../constants'; + +const getRange = (version: string) => version.slice(0, version.search(/[0-9]/)); + +const getRangeScore = (version: string | null) => { + if (version === null) return 0; + if (version === RANGE_ANY) return 8; + const range = getRange(version); + if (range === RANGE_GT) return 7; + if (range === RANGE_GTE) return 6; + if (range === RANGE_MINOR) return 5; + if (version.indexOf('.x') !== -1) return 4; + if (range === RANGE_PATCH) return 3; + if (range === RANGE_EXACT) return 2; + if (range === RANGE_LTE) return 1; + if (range === RANGE_LT) return 0; + return 0; +}; + +export const getHighestVersion = (versions: string[]): string => { + let rawHighest: string | null = null; + for (const raw of versions) { + if (raw === '*') return raw; + const version = valid(coerce(raw)); + if (version === null) continue; + const highest = valid(coerce(rawHighest)); + if ( + highest === null || + gt(version, highest) || + (eq(version, highest) && getRangeScore(raw) > getRangeScore(rawHighest)) + ) { + rawHighest = raw; + } + } + if (rawHighest === null) { + throw new Error(`Failed to find highest version of ${versions.join(', ')}`); + } + return rawHighest; +}; diff --git a/src/commands/lib/get-installations.spec.ts b/src/commands/lib/get-installations.spec.ts new file mode 100644 index 00000000..8f7a2b9b --- /dev/null +++ b/src/commands/lib/get-installations.spec.ts @@ -0,0 +1,38 @@ +import 'expect-more-jest'; +import { DependencyType } from '../../constants'; +import { getDependencies, getMismatchedDependencies } from './get-installations'; +import { SourceWrapper } from './get-wrappers'; + +const mocked = { + projects: (): SourceWrapper[] => [ + { filePath: '', contents: { dependencies: { chalk: '2.3.0' } } }, + { filePath: '', contents: { devDependencies: { jest: '22.1.4' } } }, + { filePath: '', contents: { peerDependencies: { jest: '22.1.4' } } }, + { filePath: '', contents: { dependencies: { chalk: '1.0.0' } } }, + { filePath: '', contents: { dependencies: { biggy: '0.1.0' } } }, + ], + types: (): DependencyType[] => ['dependencies', 'devDependencies', 'peerDependencies'], +}; + +const getShape = (name: string, ...installations: Array<[string, string]>) => ({ + installations: installations.map(([type, version]) => expect.objectContaining({ name, type, version })), + name, +}); + +describe('getDependencies', () => { + it('lists all dependencies and their versions', () => { + const iterator = getDependencies(mocked.types(), mocked.projects()); + expect(Array.from(iterator)).toEqual([ + getShape('chalk', ['dependencies', '2.3.0'], ['dependencies', '1.0.0']), + getShape('biggy', ['dependencies', '0.1.0']), + getShape('jest', ['devDependencies', '22.1.4'], ['peerDependencies', '22.1.4']), + ]); + }); +}); + +describe('getMismatchedDependencies', () => { + it('lists dependencies installed with different versions', () => { + const iterator = getMismatchedDependencies(mocked.types(), mocked.projects()); + expect(Array.from(iterator)).toEqual([getShape('chalk', ['dependencies', '2.3.0'], ['dependencies', '1.0.0'])]); + }); +}); diff --git a/src/commands/lib/get-installations.ts b/src/commands/lib/get-installations.ts new file mode 100644 index 00000000..75ccabff --- /dev/null +++ b/src/commands/lib/get-installations.ts @@ -0,0 +1,88 @@ +import { DependencyType } from '../../constants'; +import { SourceWrapper } from './get-wrappers'; + +export interface Installation { + /** which section the package was installed in */ + type: DependencyType; + /** eg 'lodash' */ + name: string; + /** package.json file contents */ + source: any; + /** eg '0.1.0' */ + version: string; +} + +export interface InstalledPackage { + /** eg 'lodash' */ + name: string; + /** each location this package is installed */ + installations: Installation[]; +} + +function* getInstallationsOf( + name: string, + types: DependencyType[], + wrappers: SourceWrapper[], +): Generator { + for (const type of types) { + for (const wrapper of wrappers) { + const dependencies = wrapper.contents[type]; + if (dependencies && dependencies[name]) { + yield { + name, + source: wrapper, + type, + version: dependencies[name], + }; + } + } + } +} + +export function* getDependencies(types: DependencyType[], wrappers: SourceWrapper[]): Generator { + const visited: any = {}; + for (const type of types) { + for (const wrapper of wrappers) { + if (wrapper.contents[type]) { + for (const name in wrapper.contents[type]) { + if (visited[name] === undefined) { + visited[name] = true; + yield { + installations: Array.from(getInstallationsOf(name, types, wrappers)), + name, + }; + } + } + } + } + } +} + +export function* getMismatchedDependencies( + types: DependencyType[], + wrappers: SourceWrapper[], +): Generator { + const iterator = getDependencies(types, wrappers); + for (const installedPackage of iterator) { + const { installations } = installedPackage; + const len = installations.length; + if (len > 1) { + for (let i = 1; i < len; i++) { + if (installations[i].version !== installations[i - 1].version) { + yield installedPackage; + break; + } + } + } + } +} + +export const sortByName = (a: InstalledPackage, b: InstalledPackage) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; +}; diff --git a/src/lib/get-packages.spec.ts b/src/commands/lib/get-wrappers.spec.ts similarity index 54% rename from src/lib/get-packages.spec.ts rename to src/commands/lib/get-wrappers.spec.ts index a5de07fb..95af3968 100644 --- a/src/lib/get-packages.spec.ts +++ b/src/commands/lib/get-wrappers.spec.ts @@ -1,13 +1,16 @@ import { removeSync } from 'fs-extra'; -import { getMockCommander } from '../../test/helpers'; -import { getPackages } from './get-packages'; import mock = require('mock-fs'); +import { getWrappers, SourceWrapper } from './get-wrappers'; -describe('getPackages', () => { +describe('getWrappers', () => { afterEach(() => { mock.restore(); }); + beforeAll(() => { + console.log('https://github.com/tschaub/mock-fs/issues/234'); + }); + beforeEach(() => { mock({ 'cli/a/package.json': JSON.stringify({ name: 'cli-a' }), @@ -20,28 +23,30 @@ describe('getPackages', () => { }); }); + const getShape = (name: string, filePath: string): SourceWrapper => ({ contents: { name }, filePath }); + it('prefers CLI', () => { - const program = getMockCommander(['cli/*/package.json']); - expect(getPackages(program)).toEqual([ - { data: { name: 'cli-a' }, path: 'cli/a/package.json' }, - { data: { name: 'cli-b' }, path: 'cli/b/package.json' }, + const program = { source: ['cli/*/package.json'] }; + expect(getWrappers(program)).toEqual([ + getShape('cli-a', 'cli/a/package.json'), + getShape('cli-b', 'cli/b/package.json'), ]); }); it('falls back to lerna.json if present', () => { - const program = getMockCommander([]); - expect(getPackages(program)).toEqual([ - { data: { name: 'lerna-a' }, path: 'lerna/a/package.json' }, - { data: { name: 'lerna-b' }, path: 'lerna/b/package.json' }, + const program = { source: [] }; + expect(getWrappers(program)).toEqual([ + getShape('lerna-a', 'lerna/a/package.json'), + getShape('lerna-b', 'lerna/b/package.json'), ]); }); it('resorts to defaults', () => { - const program = getMockCommander([]); + const program = { source: [] }; removeSync('lerna.json'); - expect(getPackages(program)).toEqual([ - { data: { name: 'packages-a' }, path: 'packages/a/package.json' }, - { data: { name: 'packages-b' }, path: 'packages/b/package.json' }, + expect(getWrappers(program)).toEqual([ + getShape('packages-a', 'packages/a/package.json'), + getShape('packages-b', 'packages/b/package.json'), ]); }); @@ -52,17 +57,17 @@ describe('getPackages', () => { beforeEach(() => { mock({ + 'package.json': JSON.stringify({ workspaces: ['packages/*'] }), 'packages/a/package.json': JSON.stringify({ name: 'packages-a' }), 'packages/b/package.json': JSON.stringify({ name: 'packages-b' }), - 'package.json': JSON.stringify({ workspaces: ['packages/*'] }), }); }); it('should resolve correctly', () => { - const program = getMockCommander([]); - expect(getPackages(program)).toEqual([ - { data: { name: 'packages-a' }, path: 'packages/a/package.json' }, - { data: { name: 'packages-b' }, path: 'packages/b/package.json' }, + const program = { source: [] }; + expect(getWrappers(program)).toEqual([ + getShape('packages-a', 'packages/a/package.json'), + getShape('packages-b', 'packages/b/package.json'), ]); }); }); diff --git a/src/commands/lib/get-wrappers.ts b/src/commands/lib/get-wrappers.ts new file mode 100644 index 00000000..03e25a9d --- /dev/null +++ b/src/commands/lib/get-wrappers.ts @@ -0,0 +1,50 @@ +import { readJsonSync } from 'fs-extra'; +import { sync } from 'glob'; +import { join, resolve } from 'path'; +import { ALL_PATTERNS } from '../../constants'; + +interface Options { + source: string[]; +} + +export interface Source { + bugs?: { url: string } | string; + dependencies?: { [key: string]: string }; + description?: string; + devDependencies?: { [key: string]: string }; + keywords?: string[]; + name?: string; + peerDependencies?: { [key: string]: string }; + repository?: { type: string; url: string } | string; + resolutions?: { [key: string]: string }; + scripts?: { [key: string]: string }; + version?: string; + [otherProps: string]: string | string[] | { [key: string]: string } | undefined; +} + +export interface SourceWrapper { + filePath: string; + contents: Source; +} + +const getPatternsFromConfig = (fileName: string, propName: string): string[] | null => { + const filePath = resolve(process.cwd(), fileName); + const config = readJsonSync(filePath, { throws: false }); + const isNonEmptyArray = config && config[propName] && config[propName].length > 0; + return isNonEmptyArray ? config[propName].map((dirPath: string) => join(dirPath, 'package.json')) : null; +}; + +const hasCliPatterns = (program: Options) => program.source && program.source.length > 0; +const getCliPatterns = (program: Options) => program.source; +const getYarnPatterns = () => getPatternsFromConfig('package.json', 'workspaces'); +const getLernaPatterns = () => getPatternsFromConfig('lerna.json', 'packages'); +const getDefaultPatterns = () => ALL_PATTERNS; +const resolvePattern = (pattern: string) => sync(pattern); +const reduceFlatArray = (all: string[], next: string[]) => all.concat(next); +const createWrapper = (filePath: string) => ({ contents: readJsonSync(filePath), filePath }); + +export const getWrappers = (program: Options): SourceWrapper[] => + (hasCliPatterns(program) ? getCliPatterns(program) : getYarnPatterns() || getLernaPatterns() || getDefaultPatterns()) + .map(resolvePattern) + .reduce(reduceFlatArray, []) + .map(createWrapper); diff --git a/src/commands/lib/is-semver.spec.ts b/src/commands/lib/is-semver.spec.ts new file mode 100644 index 00000000..62678c55 --- /dev/null +++ b/src/commands/lib/is-semver.spec.ts @@ -0,0 +1,19 @@ +import 'expect-more-jest'; +import { isSemver } from './is-semver'; + +describe('isSemver', () => { + it('returns whether a value is Semver', () => { + expect(isSemver('<1.2.3')).toBeTrue(); + expect(isSemver('<=1.2.3')).toBeTrue(); + expect(isSemver('1.2.3')).toBeTrue(); + expect(isSemver('1.x.x')).toBeTrue(); + expect(isSemver('1.2.x')).toBeTrue(); + expect(isSemver('~1.2.3')).toBeTrue(); + expect(isSemver('^1.2.3')).toBeTrue(); + expect(isSemver('>=1.2.3')).toBeTrue(); + expect(isSemver('>1.2.3')).toBeTrue(); + expect(isSemver('*')).toBeFalse(); + expect(isSemver('>=16.8.0 <17.0.0')).toBeFalse(); + expect(isSemver('https://github.com/npm/npm.git')).toBeFalse(); + }); +}); diff --git a/src/commands/lib/is-semver.ts b/src/commands/lib/is-semver.ts new file mode 100644 index 00000000..bbcdae7a --- /dev/null +++ b/src/commands/lib/is-semver.ts @@ -0,0 +1,28 @@ +import { + RANGE_EXACT, + RANGE_GT, + RANGE_GTE, + RANGE_LOOSE, + RANGE_LT, + RANGE_LTE, + RANGE_MINOR, + RANGE_PATCH, +} from '../../constants'; + +export const isValidSemverRange = (range: string) => + range === RANGE_EXACT || + range === RANGE_GT || + range === RANGE_GTE || + range === RANGE_LOOSE || + range === RANGE_LT || + range === RANGE_LTE || + range === RANGE_MINOR || + range === RANGE_PATCH; + +export const isSemver = (version: string) => { + return version.search(/^(~|\^|>=|>|<=|<|)?[0-9]+\.[0-9x]+\.[0-9x]+/) !== -1 && version.indexOf(' ') === -1; +}; + +export const isLooseSemver = (version: string) => { + return isSemver(version) && version.search(/\.x(\.|$)/) !== -1; +}; diff --git a/src/commands/lib/log.ts b/src/commands/lib/log.ts new file mode 100644 index 00000000..4a791c4d --- /dev/null +++ b/src/commands/lib/log.ts @@ -0,0 +1 @@ +export const log = (...args: any[]) => console.log(...args); diff --git a/src/commands/lib/write-if-changed.ts b/src/commands/lib/write-if-changed.ts new file mode 100644 index 00000000..ae116777 --- /dev/null +++ b/src/commands/lib/write-if-changed.ts @@ -0,0 +1,20 @@ +import chalk from 'chalk'; +import { writeFileSync } from 'fs-extra'; +import { EOL } from 'os'; +import { relative } from 'path'; +import { SourceWrapper } from './get-wrappers'; +import { log } from './log'; + +export const writeIfChanged = (indent: string, wrapper: SourceWrapper, mutateContents: (...args: any[]) => any) => { + const toJson = () => `${JSON.stringify(wrapper.contents, null, indent)}${EOL}`; + const shortPath = relative(process.cwd(), wrapper.filePath); + const before = toJson(); + mutateContents(); + const after = toJson(); + if (before !== after) { + writeFileSync(wrapper.filePath, after); + log(chalk.green('✓'), shortPath); + } else { + log(chalk.dim('-', shortPath)); + } +}; diff --git a/src/commands/list-mismatches.spec.ts b/src/commands/list-mismatches.spec.ts new file mode 100644 index 00000000..2a2fec5d --- /dev/null +++ b/src/commands/list-mismatches.spec.ts @@ -0,0 +1,22 @@ +import * as mock from '../../test/mock'; + +describe('listMismatches', () => { + let listMismatches: any; + let log: any; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + beforeEach(() => { + jest.mock('./lib/log', () => ({ log: jest.fn() })); + listMismatches = require('./list-mismatches').listMismatches; + log = require('./lib/log').log; + }); + + it('outputs all dependencies installed with different versions', () => { + const wrappers = [mock.wrapper('a', ['foo@0.1.0']), mock.wrapper('b', ['foo@0.2.0'])]; + listMismatches(['dependencies'], /./, wrappers); + expect(log.mock.calls).toMatchSnapshot(); + }); +}); diff --git a/src/commands/list-mismatches.ts b/src/commands/list-mismatches.ts new file mode 100644 index 00000000..00acebd5 --- /dev/null +++ b/src/commands/list-mismatches.ts @@ -0,0 +1,42 @@ +import chalk from 'chalk'; +import { DependencyType } from '../constants'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getMismatchedDependencies, InstalledPackage, sortByName } from './lib/get-installations'; +import { getWrappers, SourceWrapper } from './lib/get-wrappers'; +import { log } from './lib/log'; + +interface Options { + dev: boolean; + filter: RegExp; + peer: boolean; + prod: boolean; + source: string[]; +} + +export const listMismatches = ( + dependencyTypes: DependencyType[], + filter: RegExp, + wrappers: SourceWrapper[], +): InstalledPackage[] => { + const iterator = getMismatchedDependencies(dependencyTypes, wrappers); + const mismatches = Array.from(iterator).filter(({ name }) => name.search(filter) !== -1); + + mismatches.sort(sortByName).forEach(({ name, installations }) => { + log(chalk`{red ✕ ${name}}`); + installations.forEach(({ source, type, version }) => { + log(chalk`{dim -} ${version} {dim in ${type} of ${source.contents.name}}`); + }); + }); + + return mismatches; +}; + +export const listMismatchesFromDisk = ({ dev, filter, peer, prod, source }: Options) => { + const dependencyTypes = getDependencyTypes({ dev, peer, prod }); + const wrappers = getWrappers({ source }); + const mismatches = listMismatches(dependencyTypes, filter, wrappers); + + if (mismatches.length > 0) { + process.exit(1); + } +}; diff --git a/src/commands/list.spec.ts b/src/commands/list.spec.ts new file mode 100644 index 00000000..65dff61d --- /dev/null +++ b/src/commands/list.spec.ts @@ -0,0 +1,22 @@ +import * as mock from '../../test/mock'; + +describe('list', () => { + let list: any; + let log: any; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + beforeEach(() => { + jest.mock('./lib/log', () => ({ log: jest.fn() })); + list = require('./list').list; + log = require('./lib/log').log; + }); + + it('outputs all dependencies', () => { + const wrappers = [mock.wrapper('a', ['foo@0.1.0']), mock.wrapper('b', ['foo@0.2.0'])]; + list(['dependencies'], /./, wrappers); + expect(log.mock.calls).toMatchSnapshot(); + }); +}); diff --git a/src/commands/list.ts b/src/commands/list.ts new file mode 100644 index 00000000..34c1eaed --- /dev/null +++ b/src/commands/list.ts @@ -0,0 +1,37 @@ +import chalk from 'chalk'; +import { DependencyType } from '../constants'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getDependencies, sortByName } from './lib/get-installations'; +import { getWrappers, SourceWrapper } from './lib/get-wrappers'; +import { log } from './lib/log'; + +interface Options { + dev: boolean; + filter: RegExp; + peer: boolean; + prod: boolean; + source: string[]; +} + +export const list = (dependencyTypes: DependencyType[], filter: RegExp, wrappers: SourceWrapper[]) => { + const iterator = getDependencies(dependencyTypes, wrappers); + const packages = Array.from(iterator).filter(({ name }) => name.search(filter) !== -1); + + packages.sort(sortByName).forEach(({ name, installations }) => { + const versions = installations.map(({ version }) => version); + const uniques = Array.from(new Set(versions)); + const hasMismatches = uniques.length > 1; + const uniquesList = uniques.sort().join(', '); + const message = hasMismatches + ? chalk`{red ✕ ${name}} {dim.red ${uniquesList}}` + : chalk`{dim -} {white ${name}} {dim ${uniquesList}}`; + log(message); + }); +}; + +export const listFromDisk = ({ dev, filter, peer, prod, source }: Options) => { + const dependencyTypes = getDependencyTypes({ dev, peer, prod }); + const wrappers = getWrappers({ source }); + + list(dependencyTypes, filter, wrappers); +}; diff --git a/src/commands/set-semver-ranges.spec.ts b/src/commands/set-semver-ranges.spec.ts new file mode 100644 index 00000000..85a30169 --- /dev/null +++ b/src/commands/set-semver-ranges.spec.ts @@ -0,0 +1,78 @@ +import 'expect-more-jest'; +import * as mock from '../../test/mock'; +import { setSemverRange, setSemverRanges } from './set-semver-ranges'; + +describe('setSemverRanges', () => { + it('sets all versions to use the supplied range', () => { + const range = '~'; + const wrapper = mock.wrapper('a', ['foo@0.1.0', 'bar@2.0.0']); + setSemverRanges(range, ['dependencies'], /./, wrapper); + expect(wrapper).toEqual(mock.wrapper('a', ['foo@~0.1.0', 'bar@~2.0.0'])); + }); +}); + +describe('setSemverRange', () => { + describe('when the current value is Semver', () => { + it('sets its semver range to the given range', () => { + [ + ['', '1.2.3'], + ['>', '>1.2.3'], + ['>=', '>=1.2.3'], + ['.x', '1.x.x'], + ['<', '<1.2.3'], + ['<=', '<=1.2.3'], + ['^', '^1.2.3'], + ['~', '~1.2.3'], + ].forEach(([range, expected]) => { + expect(setSemverRange(range, '<1.2.3')).toEqual(expected); + expect(setSemverRange(range, '<=1.2.3')).toEqual(expected); + expect(setSemverRange(range, '1.2.3')).toEqual(expected); + expect(setSemverRange(range, '~1.2.3')).toEqual(expected); + expect(setSemverRange(range, '^1.2.3')).toEqual(expected); + expect(setSemverRange(range, '>=1.2.3')).toEqual(expected); + expect(setSemverRange(range, '>1.2.3')).toEqual(expected); + expect(setSemverRange(range, '*')).toEqual('*'); + expect(setSemverRange(range, 'https://github.com/npm/npm.git')).toEqual('https://github.com/npm/npm.git'); + }); + }); + }); + describe('when the current value contains a wildcard patch', () => { + it('sets its semver range to the given range', () => { + const current = '1.2.x'; + expect(setSemverRange('', current)).toEqual('1.2.0'); + expect(setSemverRange('>', current)).toEqual('>1.2.0'); + expect(setSemverRange('>=', current)).toEqual('>=1.2.0'); + expect(setSemverRange('.x', current)).toEqual('1.x.x'); + expect(setSemverRange('<', current)).toEqual('<1.2.0'); + expect(setSemverRange('<=', current)).toEqual('<=1.2.0'); + expect(setSemverRange('^', current)).toEqual('^1.2.0'); + expect(setSemverRange('~', current)).toEqual('~1.2.0'); + }); + }); + describe('when the current value contains a wildcard minor and patch', () => { + it('sets its semver range to the given range', () => { + const current = '1.x.x'; + expect(setSemverRange('', current)).toEqual('1.0.0'); + expect(setSemverRange('>', current)).toEqual('>1.0.0'); + expect(setSemverRange('>=', current)).toEqual('>=1.0.0'); + expect(setSemverRange('.x', current)).toEqual(current); + expect(setSemverRange('<', current)).toEqual('<1.0.0'); + expect(setSemverRange('<=', current)).toEqual('<=1.0.0'); + expect(setSemverRange('^', current)).toEqual('^1.0.0'); + expect(setSemverRange('~', current)).toEqual('~1.0.0'); + }); + }); + describe('when the current value contains multiple versions', () => { + it('leaves the version unchanged', () => { + const current = '>=16.8.0 <17.0.0'; + expect(setSemverRange('', current)).toEqual(current); + expect(setSemverRange('>', current)).toEqual(current); + expect(setSemverRange('>=', current)).toEqual(current); + expect(setSemverRange('.x', current)).toEqual(current); + expect(setSemverRange('<', current)).toEqual(current); + expect(setSemverRange('<=', current)).toEqual(current); + expect(setSemverRange('^', current)).toEqual(current); + expect(setSemverRange('~', current)).toEqual(current); + }); + }); +}); diff --git a/src/commands/set-semver-ranges.ts b/src/commands/set-semver-ranges.ts new file mode 100644 index 00000000..bef67245 --- /dev/null +++ b/src/commands/set-semver-ranges.ts @@ -0,0 +1,54 @@ +import { DependencyType, RANGE_LOOSE } from '../constants'; +import { getDependencyTypes } from './lib/get-dependency-types'; +import { getDependencies } from './lib/get-installations'; +import { getWrappers, SourceWrapper } from './lib/get-wrappers'; +import { isLooseSemver, isSemver, isValidSemverRange } from './lib/is-semver'; +import { writeIfChanged } from './lib/write-if-changed'; + +interface Options { + dev: boolean; + filter: RegExp; + indent: string; + peer: boolean; + prod: boolean; + semverRange: string; + source: string[]; +} + +export const setSemverRange = (range: string, version: string) => { + if (!isSemver(version) || !isValidSemverRange(range)) { + return version; + } + const nextVersion = isLooseSemver(version) ? version.replace(/\.x/g, '.0') : version; + const from1stNumber = nextVersion.search(/[0-9]/); + const from1stDot = nextVersion.indexOf('.'); + return range === RANGE_LOOSE + ? `${nextVersion.slice(from1stNumber, from1stDot)}.x.x` + : `${range}${nextVersion.slice(from1stNumber)}`; +}; + +export const setSemverRanges = ( + semverRange: string, + dependencyTypes: DependencyType[], + filter: RegExp, + wrapper: SourceWrapper, +) => { + const iterator = getDependencies(dependencyTypes, [wrapper]); + for (const installedPackage of iterator) { + if (installedPackage.name.search(filter) !== -1) { + for (const installation of installedPackage.installations) { + const { name, type, version } = installation; + installation.source.contents[type][name] = setSemverRange(semverRange, version); + } + } + } +}; + +export const setSemverRangesToDisk = ({ dev, filter, indent, peer, prod, semverRange, source }: Options) => { + const dependencyTypes = getDependencyTypes({ dev, peer, prod }); + getWrappers({ source }).forEach((wrapper) => { + writeIfChanged(indent, wrapper, () => { + setSemverRanges(semverRange, dependencyTypes, filter, wrapper); + }); + }); +}; diff --git a/src/constants.ts b/src/constants.ts index fb13d879..2ac13d3b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,10 +1,5 @@ -import { IManifestKey } from './typings'; - -export const DEPENDENCY_TYPES: IManifestKey[] = [ - 'dependencies', - 'devDependencies', - 'peerDependencies', -]; +export type DependencyType = 'dependencies' | 'devDependencies' | 'peerDependencies'; +export const DEPENDENCY_TYPES: DependencyType[] = ['dependencies', 'devDependencies', 'peerDependencies']; export const SORT_AZ = [ 'contributors', @@ -17,10 +12,11 @@ export const SORT_AZ = [ ]; export const SORT_FIRST = ['name', 'description', 'version', 'author']; -export const VERSION = require('../package.json').version; export const GREATER = 1; export const LESSER = -1; export const SAME = 0; + +export type ValidRange = '*' | '' | '>' | '>=' | '.x' | '<' | '<=' | '^' | '~'; export const RANGE_ANY = '*'; export const RANGE_EXACT = ''; export const RANGE_GT = '>'; @@ -31,7 +27,7 @@ export const RANGE_LTE = '<='; export const RANGE_MINOR = '^'; export const RANGE_PATCH = '~'; -export const SEMVER_ORDER = [ +export const SEMVER_ORDER: ValidRange[] = [ RANGE_LT, RANGE_LTE, RANGE_EXACT, @@ -46,71 +42,30 @@ const DEFAULT_INDENT = ' '; const DEFAULT_SEMVER_RANGE = RANGE_EXACT; const MONOREPO_PATTERN = 'package.json'; const PACKAGES_PATTERN = 'packages/*/package.json'; -const ALL_PATTERNS = [MONOREPO_PATTERN, PACKAGES_PATTERN]; - -export const FIX_MISMATCHES = { - command: 'fix-mismatches', - description: - 'set dependencies used with different versions to the same version', -}; - -export const FORMAT = { - command: 'format', - description: 'sort and shorten properties according to a convention', -}; - -export const LIST = { - command: 'list', - description: 'list every dependency used in your packages', -}; - -export const LIST_MISMATCHES = { - command: 'list-mismatches', - description: - 'list every dependency used with different versions in your packages', -}; - -export const SET_SEMVER_RANGES = { - command: 'set-semver-ranges', - description: 'set semver ranges to the given format', -}; - -export const OPTION_SEMVER_RANGE = { - default: DEFAULT_SEMVER_RANGE, - description: - `${RANGE_LT}, ${RANGE_LTE}, "${RANGE_EXACT}", ${RANGE_PATCH}, ${RANGE_MINOR}, ` + - `${RANGE_GTE}, ${RANGE_GT}, or ${RANGE_ANY}. defaults to "${DEFAULT_SEMVER_RANGE}"`, - spec: '-r, --semver-range ', -}; - -export const OPTION_SOURCES = { - default: ALL_PATTERNS, - 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', -}; - -export const OPTIONS_FILTER_DEPENDENCIES = { - description: 'regex for depdendency filter', - spec: '-f, --filter [pattern]', -}; - -export const OPTION_INDENT = { - default: DEFAULT_INDENT, - description: `override indentation. defaults to "${DEFAULT_INDENT}"`, - spec: '-i, --indent [value]', +export const ALL_PATTERNS = [MONOREPO_PATTERN, PACKAGES_PATTERN]; + +interface OptionsByName { + dev: [string, string]; + filter: [string, string]; + indent: [string, string]; + peer: [string, string]; + prod: [string, string]; + semverRange: [string, string]; + source: [string, string, (...args: any) => any]; +} + +export const option: OptionsByName = { + dev: ['-d, --dev', 'include devDependencies'], + filter: ['-f, --filter [pattern]', 'regex for dependency filter'], + indent: ['-i, --indent [value]', `override indentation. defaults to "${DEFAULT_INDENT}"`], + peer: ['-P, --peer', 'include peerDependencies'], + prod: ['-p, --prod', 'include dependencies'], + semverRange: ['-r, --semver-range ', `see supported ranges below. defaults to "${DEFAULT_SEMVER_RANGE}"`], + source: [ + '-s, --source [pattern]', + 'glob pattern for package.json files to read from', + function collect(value: string, values: string[] = []) { + return [...values, value]; + }, + ], }; diff --git a/src/fix-mismatches.spec.ts b/src/fix-mismatches.spec.ts deleted file mode 100644 index 749d112f..00000000 --- a/src/fix-mismatches.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { readJsonSync } from 'fs-extra'; -import { getFixture, getMockCommander } from '../test/helpers'; -import { run } from './fix-mismatches'; -import { IManifest } from './typings'; -import mock = require('mock-fs'); - -describe('fix-mismatches', () => { - let spyConsole: any; - - afterAll(() => { - mock.restore(); - }); - - beforeAll(async () => { - const [one, two, three] = getFixture('exact').data as IManifest[]; - const sources = [ - '/path/1/package.json', - '/path/2/package.json', - '/path/3/package.json', - ]; - const program = getMockCommander(sources, '^((?!ignore).)*$'); - mock({ - '/path/1/package.json': JSON.stringify(one), - '/path/2/package.json': JSON.stringify(two), - '/path/3/package.json': JSON.stringify(three), - }); - const noop = () => undefined; - spyConsole = jest.spyOn(console, 'log').mockImplementation(noop); - await run(program); - }); - - it('sets the version of dependencies with different versions to the newest of those versions found', () => { - expect(readJsonSync('/path/1/package.json')).toEqual( - expect.objectContaining({ - dependencies: { chalk: '2.3.0', commander: '2.13.0', ignore: '1.0.0' }, - devDependencies: { - jest: '22.1.4', - prettier: '1.10.2', - rimraf: '2.6.2', - }, - peerDependencies: { gulp: '*' }, - }), - ); - expect(readJsonSync('/path/2/package.json')).toEqual( - expect.objectContaining({ - dependencies: { chalk: '2.3.0', ignore: '2.0.0' }, - devDependencies: { jest: '22.1.4' }, - }), - ); - expect(readJsonSync('/path/3/package.json')).toEqual( - expect.objectContaining({ - devDependencies: { - npm: 'https://github.com/npm/npm.git', - prettier: '1.10.2', - }, - peerDependencies: { gulp: '*' }, - }), - ); - }); -}); diff --git a/src/fix-mismatches.ts b/src/fix-mismatches.ts deleted file mode 100644 index 8b7504fb..00000000 --- a/src/fix-mismatches.ts +++ /dev/null @@ -1,77 +0,0 @@ -import chalk from 'chalk'; -import { writeJson } from 'fs-extra'; -import _ = require('lodash'); -import { relative } from 'path'; -import { - OPTION_INDENT, - OPTION_SOURCES, - OPTIONS_DEV, - OPTIONS_FILTER_DEPENDENCIES, - OPTIONS_PEER, - OPTIONS_PROD, -} from './constants'; -import { collect } from './lib/collect'; -import { getDependencyTypes } from './lib/get-dependency-types'; -import { getIndent } from './lib/get-indent'; -import { getPackages } from './lib/get-packages'; -import { getMismatchedVersionsByName } from './lib/get-versions-by-name'; -import { getNewest } from './lib/version'; -import { CommanderApi, IManifest } from './typings'; - -export const run = async (program: CommanderApi) => { - 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) - .option(OPTION_INDENT.spec, OPTION_INDENT.description) - .option( - OPTIONS_FILTER_DEPENDENCIES.spec, - OPTIONS_FILTER_DEPENDENCIES.description, - ) - .parse(process.argv); - - const pkgs = getPackages(program); - const dependencyTypes = getDependencyTypes(program); - const indent = getIndent(program); - const mismatchedVersionsByName = getMismatchedVersionsByName( - dependencyTypes, - pkgs, - program.filter, - ); - - await Promise.all( - pkgs.map(({ data, path }) => { - const nextData: IManifest = JSON.parse(JSON.stringify(data)); - const shortPath = `./${relative('.', path)}`; - const changes: string[][] = []; - Object.entries(mismatchedVersionsByName).forEach(([name, versions]) => { - const newest = getNewest(versions); - dependencyTypes.forEach((type) => { - if ( - nextData[type] && - nextData[type][name] && - nextData[type][name] !== newest - ) { - changes.push([name, nextData[type][name], newest]); - nextData[type][name] = newest; - } - }); - }); - if (changes.length > 0) { - changes.forEach(([name, from, to]) => { - console.log( - chalk.bgYellow.black(' FIXED '), - chalk.blue(shortPath), - name, - chalk.red(from), - '→', - chalk.green(to), - ); - }); - return writeJson(path, nextData, { spaces: indent }); - } - console.log(chalk.bgGreen.black(' VALID '), chalk.blue(shortPath)); - }), - ); -}; diff --git a/src/format.spec.ts b/src/format.spec.ts deleted file mode 100644 index 005ba411..00000000 --- a/src/format.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { readJsonSync } from 'fs-extra'; -import { getFixture, getMockCommander, shuffleObject } from '../test/helpers'; -import { SORT_FIRST } from './constants'; -import { run } from './format'; -import { IManifest } from './typings'; -import mock = require('mock-fs'); - -describe('format', () => { - let minimal: IManifest; - let untidy: IManifest; - - beforeAll(async () => { - const program = getMockCommander([ - '/minimal/package.json', - '/untidy/package.json', - ]); - mock({ - '/minimal/package.json': JSON.stringify({ - name: 'minimal', - version: '0.0.0', - }), - '/untidy/package.json': getFixture('untidy', shuffleObject).json, - }); - await run(program); - minimal = readJsonSync('/minimal/package.json'); - untidy = readJsonSync('/untidy/package.json'); - mock.restore(); - }); - - it('sorts specified keys to the top of package.json', () => { - expect(Object.keys(untidy).slice(0, SORT_FIRST.length)).toEqual(SORT_FIRST); - }); - - it('sorts remaining keys alphabetically', () => { - expect(Object.keys(untidy).slice(SORT_FIRST.length - 1)).toEqual([ - 'author', - 'bin', - 'bugs', - 'dependencies', - 'devDependencies', - 'files', - 'homepage', - 'keywords', - 'license', - 'main', - 'peerDependencies', - 'repository', - 'scripts', - ]); - }); - - it('sorts "dependencies" alphabetically', () => { - expect(minimal).not.toHaveProperty('dependencies'); - expect(untidy.dependencies).toEqual({ - arnold: '5.0.0', - dog: '2.13.0', - guybrush: '7.1.1', - mango: '2.3.0', - }); - }); - - it('sorts "devDependencies" alphabetically', () => { - expect(minimal).not.toHaveProperty('devDependencies'); - expect(untidy.devDependencies).toEqual({ - stroopwafel: '4.4.2', - waldorf: '22.1.4', - }); - }); - - it('sorts "files" alphabetically', () => { - expect(minimal).not.toHaveProperty('files'); - expect(untidy.files).toEqual(['assets', 'dist']); - }); - - it('sorts "keywords" alphabetically', () => { - expect(minimal).not.toHaveProperty('keywords'); - expect(untidy.keywords).toEqual(['thing', 'those', 'whatsits']); - }); - - it('sorts "peerDependencies" alphabetically', () => { - expect(minimal).not.toHaveProperty('peerDependencies'); - expect(untidy.peerDependencies).toEqual({ - giftwrap: '0.1.2', - jambalaya: '6.1.4', - zoolander: '1.4.25', - }); - }); - - it('sorts "scripts" alphabetically', () => { - expect(minimal).not.toHaveProperty('scripts'); - expect(untidy.scripts).toEqual({ - build: 'tsc', - format: 'prettier', - lint: 'tslint', - test: 'jest', - }); - }); - - it('uses shorthand "bugs"', () => { - expect(minimal).not.toHaveProperty('bugs'); - expect(untidy.bugs).toEqual('https://github.com/JaneDoe/do-it/issues'); - }); - - it('uses shorthand "repository"', () => { - expect(minimal).not.toHaveProperty('repository'); - expect(untidy.repository).toEqual('JaneDoe/do-it'); - }); -}); diff --git a/src/format.ts b/src/format.ts deleted file mode 100644 index 03459004..00000000 --- a/src/format.ts +++ /dev/null @@ -1,86 +0,0 @@ -import chalk from 'chalk'; -import { writeJson } from 'fs-extra'; -import _ = require('lodash'); -import { relative } from 'path'; -import { - OPTION_INDENT, - OPTION_SOURCES, - SORT_AZ, - SORT_FIRST, -} from './constants'; -import { collect } from './lib/collect'; -import { getIndent } from './lib/get-indent'; -import { getPackages } from './lib/get-packages'; -import { CommanderApi, IManifest } from './typings'; - -export const run = async (program: CommanderApi) => { - const shortenBugs = (manifest: IManifest): IManifest => { - const bugsUrl = _.get(manifest, 'bugs.url') as string; - return bugsUrl ? { ...manifest, bugs: bugsUrl } : manifest; - }; - - const shortenRepository = (manifest: IManifest): IManifest => { - const repoUrl = _.get(manifest, 'repository.url', '') as string; - return repoUrl.includes('github.com') - ? { ...manifest, repository: repoUrl.split('github.com/')[1] } - : manifest; - }; - - const sortObject = (obj: object) => - _(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) - .option(OPTION_INDENT.spec, OPTION_INDENT.description) - .parse(process.argv); - - const pkgs = getPackages(program); - const indent = getIndent(program); - - await Promise.all( - pkgs.map(({ data, path }) => { - const nextData = sortManifest(shortenBugs(shortenRepository(data))); - const hasChanged = JSON.stringify(nextData) !== JSON.stringify(data); - const shortPath = `./${relative('.', path)}`; - if (hasChanged) { - console.log(chalk.bgYellow.black(' FIXED '), chalk.blue(shortPath)); - return writeJson(path, nextData, { spaces: indent }); - } - console.log(chalk.bgGreen.black(' VALID '), chalk.blue(shortPath)); - }), - ); -}; diff --git a/src/lib/collect.spec.ts b/src/lib/collect.spec.ts deleted file mode 100644 index 59574434..00000000 --- a/src/lib/collect.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { collect } from './collect'; - -describe('collect', () => { - it('concatenates values', () => { - expect(collect('baz', ['foo', 'bar'])).toEqual(['foo', 'bar', 'baz']); - }); -}); diff --git a/src/lib/collect.ts b/src/lib/collect.ts deleted file mode 100644 index 1e84093c..00000000 --- a/src/lib/collect.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const collect = (value: string, values: string[] = []) => [ - ...values, - value, -]; diff --git a/src/lib/get-dependency-types.spec.ts b/src/lib/get-dependency-types.spec.ts deleted file mode 100644 index 2f4abc0e..00000000 --- a/src/lib/get-dependency-types.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CommanderApi } from '../typings'; -import { getDependencyTypes } from './get-dependency-types'; - -describe('getDependencyTypes', () => { - it('returns all if none are set or true if any are set', () => { - [ - [ - { dev: false, peer: false, prod: false }, - ['dependencies', 'devDependencies', 'peerDependencies'], - ], - [{ dev: true, peer: false, prod: false }, ['devDependencies']], - [{ dev: false, peer: true, prod: false }, ['peerDependencies']], - [{ dev: false, peer: false, prod: true }, ['dependencies']], - ].forEach(([program, expected]) => { - expect(getDependencyTypes((program as unknown) as CommanderApi)).toEqual( - expected, - ); - }); - }); -}); diff --git a/src/lib/get-indent.ts b/src/lib/get-indent.ts deleted file mode 100644 index 7e163c12..00000000 --- a/src/lib/get-indent.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { OPTION_INDENT } from '../constants'; -import { CommanderApi } from '../typings'; - -export type GetIndent = (program: CommanderApi) => string; - -export const getIndent: GetIndent = (program) => - program.indent || OPTION_INDENT.default; diff --git a/src/lib/get-packages.ts b/src/lib/get-packages.ts deleted file mode 100644 index 302c5046..00000000 --- a/src/lib/get-packages.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { readJsonSync } from 'fs-extra'; -import { sync } from 'glob'; -import { join, resolve } from 'path'; -import { OPTION_SOURCES } from '../constants'; -import { CommanderApi, IManifestDescriptor } from '../typings'; - -const getYarnWorkspaces = (): string[] | null => { - const rootPackageJson = resolve(process.cwd(), 'package.json'); - const pkgJson = readJsonSync(rootPackageJson, { throws: false }); - if (pkgJson && pkgJson.workspaces && pkgJson.workspaces.length) { - return pkgJson.workspaces.map((glob: string) => join(glob, 'package.json')); - } - return null; -}; - -const getLernaPackages = (): string[] | null => { - const lernaPath = resolve(process.cwd(), 'lerna.json'); - const lerna = readJsonSync(lernaPath, { throws: false }); - if (lerna && lerna.packages && lerna.packages.length) { - return lerna.packages.map((glob: string) => join(glob, 'package.json')); - } - return null; -}; - -export const getSources = (program: CommanderApi): string[] => { - if (program.source && program.source.length) { - return program.source; - } - return getYarnWorkspaces() || getLernaPackages() || OPTION_SOURCES.default; -}; - -export const getPackages = (program: CommanderApi): IManifestDescriptor[] => - getSources(program) - .reduce( - (filePaths, pattern) => filePaths.concat(sync(pattern)), - [], - ) - .map((filePath) => ({ - data: readJsonSync(filePath), - path: filePath, - })); diff --git a/src/lib/get-versions-by-name.ts b/src/lib/get-versions-by-name.ts deleted file mode 100644 index 63093e7a..00000000 --- a/src/lib/get-versions-by-name.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { IManifestDescriptor, IManifestKey } from '../typings'; - -export interface IVersionsByName { - [name: string]: string[]; -} - -export type GetVersionsByName = ( - dependencyTypes: IManifestKey[], - pkgs: IManifestDescriptor[], - dependencyFilterPattern?: string, -) => IVersionsByName; - -export const getDependencyFilter = (dependencyFilterPattern?: string) => { - if (!dependencyFilterPattern) { - return () => true; - } - const dependencyMatcher = new RegExp(dependencyFilterPattern); - - return (dependencyName: string) => dependencyMatcher.test(dependencyName); -}; - -export const getVersionsByName: GetVersionsByName = ( - dependencyTypes, - pkgs, - dependencyFilterPattern, -) => { - const dependencyFilter = getDependencyFilter(dependencyFilterPattern); - 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 (!dependencyFilter(name)) { - continue; - } - 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, - dependencyFilterPattern, -) => { - const dependencyFilter = getDependencyFilter(dependencyFilterPattern); - const mismatchedVersionsByName: IVersionsByName = {}; - const versionsByName = getVersionsByName( - dependencyTypes, - pkgs, - dependencyFilterPattern, - ); - for (const type of dependencyTypes) { - for (const pkg of pkgs) { - const dependencies = pkg.data[type]; - if (dependencies) { - for (const name in dependencies) { - if (!dependencyFilter(name)) { - continue; - } - if (dependencies.hasOwnProperty(name)) { - if (versionsByName[name].length > 1) { - mismatchedVersionsByName[name] = versionsByName[name]; - } - } - } - } - } - } - return mismatchedVersionsByName; -}; diff --git a/src/lib/version.spec.ts b/src/lib/version.spec.ts deleted file mode 100644 index 3059cef2..00000000 --- a/src/lib/version.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import _ = require('lodash'); -import { getNewest, getVersionRange, sortBySemver } from './version'; - -describe('getNewest', () => { - it('returns the newest version from an array of versions', () => { - const a = ['<1.0.0']; - const b = _.shuffle([...a, '<=1.0.0']); - const c = _.shuffle([...b, '1.0.0']); - const d = _.shuffle([...c, '~1.0.0']); - const e = _.shuffle([...d, '1.x.x']); - const f = _.shuffle([...e, '^1.0.0']); - const g = _.shuffle([...f, '>=1.0.0']); - const h = _.shuffle([...g, '>1.0.0']); - const i = _.shuffle([...h, '*']); - const j = _.shuffle([...i, 'http://asdf.com/asdf.tar.gz']); - const k = _.shuffle([...j, 'file:../foo/bar']); - const l = _.shuffle([...k, 'latest']); - const m = _.shuffle([...l, 'git+ssh://git@github.com:npm/cli.git#v1.0.27']); - const n = _.shuffle([...m, 'git+ssh://git@github.com:npm/cli#semver:^5.0']); - const o = _.shuffle([...n, 'git+https://isaacs@github.com/npm/cli.git']); - const p = _.shuffle([...o, 'git://github.com/npm/cli.git#v1.0.27']); - const q = _.shuffle([...p, 'expressjs/express']); - const r = _.shuffle([...q, 'mochajs/mocha#4727d357ea']); - const s = _.shuffle([...r, 'user/repo#feature/branch']); - // valid semver - expect(getNewest(a)).toEqual('<1.0.0'); - expect(getNewest(b)).toEqual('<=1.0.0'); - expect(getNewest(c)).toEqual('1.0.0'); - expect(getNewest(d)).toEqual('~1.0.0'); - expect(getNewest(e)).toEqual('1.x.x'); - expect(getNewest(f)).toEqual('^1.0.0'); - expect(getNewest(g)).toEqual('>=1.0.0'); - expect(getNewest(h)).toEqual('>1.0.0'); - expect(getNewest(i)).toEqual('*'); - // invalid semver - expect(getNewest(j)).toEqual('*'); - expect(getNewest(k)).toEqual('*'); - expect(getNewest(l)).toEqual('*'); - expect(getNewest(m)).toEqual('*'); - expect(getNewest(n)).toEqual('*'); - expect(getNewest(o)).toEqual('*'); - expect(getNewest(p)).toEqual('*'); - expect(getNewest(q)).toEqual('*'); - expect(getNewest(r)).toEqual('*'); - expect(getNewest(s)).toEqual('*'); - }); -}); - -describe('getVersionRange', () => { - it('gets the version range from a semver dependency', () => { - [ - ['*', '*'], - ['>1.0.0', '>'], - ['>=1.0.0', '>='], - ['^1.0.0', '^'], - ['~1.0.0', '~'], - ['1.x.x', '.x'], - ['1.0.0', ''], - ['<=1.0.0', '<='], - ['<1.0.0', '<'], - ['http://asdf.com/asdf.tar.gz', ''], - ['file:../foo/bar', ''], - ['latest', ''], - ['git+ssh://git@github.com:npm/cli.git#v1.0.27', ''], - ['git+ssh://git@github.com:npm/cli#semver:^5.0', ''], - ['git+https://isaacs@github.com/npm/cli.git', ''], - ['git://github.com/npm/cli.git#v1.0.27', ''], - ['expressjs/express"', ''], - ['mochajs/mocha#4727d357ea"', ''], - ['user/repo#feature/branch', ''], - ].forEach(([version, expected]) => { - expect(getVersionRange(version)).toEqual(expected); - }); - }); -}); - -describe('sortBySemver', () => { - it('returns an ordered array of semver versions', () => { - _.times(10, () => { - const unordered = [ - 'http://asdf.com/asdf.tar.gz', - 'file:../foo/bar', - 'latest', - 'git+ssh://git@github.com:npm/cli.git#v1.0.27', - 'git+ssh://git@github.com:npm/cli#semver:^5.0', - 'git+https://isaacs@github.com/npm/cli.git', - 'git://github.com/npm/cli.git#v1.0.27', - 'expressjs/express', - 'mochajs/mocha#4727d357ea', - 'user/repo#feature/branch', - '^1.0.0', - '^1.0.1', - '^1.0.0', - '1.0.0', - '1.0.1', - '1.0.0', - '*', - '1.x.x', - '*', - '<=1.0.0', - '<=1.0.1', - '<=1.0.0', - '>1.0.0', - '>1.0.1', - '>1.0.0', - '>=1.0.0', - '>=1.0.1', - '>=1.0.0', - '<1.0.0', - '<1.0.1', - '<1.0.0', - '~1.0.0', - '~1.0.1', - '~1.0.0', - ]; - const expectedOrder = [ - '<1.0.0', - '<1.0.0', - '<=1.0.0', - '<=1.0.0', - '1.0.0', - '1.0.0', - '~1.0.0', - '~1.0.0', - '^1.0.0', - '^1.0.0', - '1.x.x', - '>=1.0.0', - '>=1.0.0', - '>1.0.0', - '>1.0.0', - '<1.0.1', - '<=1.0.1', - '1.0.1', - '~1.0.1', - '^1.0.1', - '>=1.0.1', - '>1.0.1', - '*', - '*', - ]; - const shuffled = _.shuffle(unordered); - expect(sortBySemver(shuffled)).toEqual(expectedOrder); - }); - }); -}); diff --git a/src/lib/version.ts b/src/lib/version.ts deleted file mode 100644 index 61ad3604..00000000 --- a/src/lib/version.ts +++ /dev/null @@ -1,64 +0,0 @@ -import _ = require('lodash'); -import * as semver from 'semver'; -import { GREATER, LESSER, SAME, SEMVER_ORDER } from '../constants'; - -const isSemver = (version: string) => - version === '*' || - version.search(/^(<|<=|~|\^|>=|>)?([0-9]+|x)\.([0-9]+|x)\.([0-9]+|x)/) !== -1; - -export const getNewest = (versions: string[]): string => { - const sorted = sortBySemver(versions); - return sorted[sorted.length - 1]; -}; - -export const getVersionNumber = (version: string): string => - version.slice(version.search(/[0-9]/), version.length); - -export const getVersionRange = (version: string): string => { - if (isSemver(version)) { - return version.includes('.x') ? '.x' : version.split(/[0-9]/)[0]; - } - return ''; -}; - -export const sortBySemver = (versions: string[]): string[] => - versions - .concat() - .filter(isSemver) - .sort() - .sort((a: string, b: string) => { - if (a === '*') { - return GREATER; - } - if (b === '*') { - return LESSER; - } - if (a === b) { - return SAME; - } - let aRange = getVersionRange(a); - let bRange = getVersionRange(b); - let aNumber = getVersionNumber(a); - let bNumber = getVersionNumber(b); - if (aNumber.indexOf('.x') !== -1) { - aNumber = aNumber.split('.x').join('.0'); - aRange = '^'; - } - if (bNumber.indexOf('.x') !== -1) { - bNumber = bNumber.split('.x').join('.0'); - bRange = '^'; - } - if (semver.gt(aNumber, bNumber)) { - return GREATER; - } - if (semver.lt(aNumber, bNumber)) { - return LESSER; - } - if (SEMVER_ORDER.indexOf(aRange) > SEMVER_ORDER.indexOf(bRange)) { - return GREATER; - } - if (SEMVER_ORDER.indexOf(aRange) < SEMVER_ORDER.indexOf(bRange)) { - return LESSER; - } - return SAME; - }); diff --git a/src/list-mismatches.spec.ts b/src/list-mismatches.spec.ts deleted file mode 100644 index b8a0bb98..00000000 --- a/src/list-mismatches.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import mock = require('mock-fs'); -import { getFixture, getMockCommander } from '../test/helpers'; -import { run } from './list-mismatches'; -import { IManifest } from './typings'; - -describe('list-mismatches', () => { - let spyConsole: any; - let spyProcess: any; - - beforeAll(async () => { - const [one, two, three] = getFixture('exact').data as IManifest[]; - const program = getMockCommander( - ['/path/1/package.json', '/path/2/package.json', '/path/3/package.json'], - '^((?!ignore).)*$', - ); - mock({ - '/path/1/package.json': JSON.stringify(one), - '/path/2/package.json': JSON.stringify(two), - '/path/3/package.json': JSON.stringify(three), - }); - const noop = () => undefined; - spyConsole = jest.spyOn(console, 'log').mockImplementation(noop); - spyProcess = jest.spyOn(process, 'exit').mockImplementation(noop as any); - await run(program); - mock.restore(); - }); - - it('returns an index of dependencies used with different versions', () => { - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('gulp'), - expect.stringContaining('0.9.1, *'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('chalk'), - expect.stringContaining('2.3.0, 1.0.0'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('jest'), - expect.stringContaining('22.1.3, 22.1.4'), - ); - expect(spyConsole).not.toHaveBeenCalledWith( - expect.stringContaining('ignore'), - ); - }); - - it('returns an error exit code when mismatches are found', () => { - expect(spyProcess).toHaveBeenCalledWith(1); - }); - - it('returns exit code 0 when no mismatches are found', async () => { - const program = getMockCommander([ - '/bar/package.json', - '/foo/package.json', - ]); - mock({ - '/bar/package.json': JSON.stringify({ - dependencies: { bar: '1.0.0' }, - name: 'bar', - }), - '/foo/package.json': JSON.stringify({ - dependencies: { bar: '1.0.0' }, - name: 'foo', - }), - }); - spyProcess.mockClear(); - await run(program); - mock.restore(); - expect(spyProcess).not.toHaveBeenCalled(); - }); -}); diff --git a/src/list-mismatches.ts b/src/list-mismatches.ts deleted file mode 100644 index 18df8f62..00000000 --- a/src/list-mismatches.ts +++ /dev/null @@ -1,42 +0,0 @@ -import chalk from 'chalk'; -import _ = require('lodash'); -import { - OPTION_SOURCES, - OPTIONS_DEV, - OPTIONS_FILTER_DEPENDENCIES, - 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 { CommanderApi } from './typings'; - -export const run = async (program: CommanderApi) => { - 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) - .option( - OPTIONS_FILTER_DEPENDENCIES.spec, - OPTIONS_FILTER_DEPENDENCIES.description, - ) - .parse(process.argv); - const dependencyTypes = getDependencyTypes(program); - const pkgs = getPackages(program); - const mismatchedVersionsByName = getMismatchedVersionsByName( - dependencyTypes, - pkgs, - program.filter, - ); - - _.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.spec.ts b/src/list.spec.ts deleted file mode 100644 index ee42b7c1..00000000 --- a/src/list.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import mock = require('mock-fs'); -import { getFixture, getMockCommander } from '../test/helpers'; -import { run } from './list'; -import { IManifest } from './typings'; - -describe('list', () => { - let spyConsole: any; - - beforeAll(async () => { - const [one, two, three] = getFixture('exact').data as IManifest[]; - const program = getMockCommander( - ['/path/1/package.json', '/path/2/package.json', '/path/3/package.json'], - '^((?!ignore).)*$', - ); - mock({ - '/path/1/package.json': JSON.stringify(one), - '/path/2/package.json': JSON.stringify(two), - '/path/3/package.json': JSON.stringify(three), - }); - const noop = () => undefined; - spyConsole = jest.spyOn(console, 'log').mockImplementation(noop); - await run(program); - mock.restore(); - }); - - it('returns an index of dependencies used with different versions', () => { - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('chalk'), - expect.stringContaining('2.3.0, 1.0.0'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('commander'), - expect.stringContaining('2.13.0'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('jest'), - expect.stringContaining('22.1.3, 22.1.4'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('prettier'), - expect.stringContaining('1.10.2'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('rimraf'), - expect.stringContaining('2.6.2'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('npm'), - expect.stringContaining('https://github.com/npm/npm.git'), - ); - expect(spyConsole).toHaveBeenCalledWith( - expect.stringContaining('gulp'), - expect.stringContaining('0.9.1, *'), - ); - expect(spyConsole).not.toHaveBeenCalledWith( - expect.stringContaining('ignore'), - ); - }); -}); diff --git a/src/list.ts b/src/list.ts deleted file mode 100644 index 3b57314f..00000000 --- a/src/list.ts +++ /dev/null @@ -1,46 +0,0 @@ -import chalk from 'chalk'; -import _ = require('lodash'); -import { - OPTION_SOURCES, - OPTIONS_DEV, - OPTIONS_FILTER_DEPENDENCIES, - 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'; -import { CommanderApi } from './typings'; - -export const run = async (program: CommanderApi) => { - 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) - .option( - OPTIONS_FILTER_DEPENDENCIES.spec, - OPTIONS_FILTER_DEPENDENCIES.description, - ) - .parse(process.argv); - - const dependencyTypes = getDependencyTypes(program); - const pkgs = getPackages(program); - const versionsByName = getVersionsByName( - dependencyTypes, - pkgs, - program.filter, - ); - - _(versionsByName) - .entries() - .sortBy('0') - .each(([name, versions]) => { - 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/set-semver-ranges.spec.ts b/src/set-semver-ranges.spec.ts deleted file mode 100644 index 9cdd6d38..00000000 --- a/src/set-semver-ranges.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readJsonSync } from 'fs-extra'; -import { getFixture, getMockCommander } 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 { run } from './set-semver-ranges'; -import mock = require('mock-fs'); - -describe('set-semver-ranges', () => { - const ranges = [ - [RANGE_ANY, 'any'], - [RANGE_EXACT, 'exact'], - [RANGE_GT, 'gt'], - [RANGE_GTE, 'gte'], - [RANGE_LOOSE, 'loose'], - [RANGE_LT, 'lt'], - [RANGE_LTE, 'lte'], - [RANGE_MINOR, 'minor'], - [RANGE_PATCH, 'patch'], - ].map(([range, name]) => ({ - data: getFixture(name).data[0], - filePath: `/path/${name}/package.json`, - range, - })); - const unsupported = [RANGE_ANY, RANGE_LOOSE]; - const sources = ranges.map(({ filePath }) => filePath); - const filesystem = ranges.reduce( - (obj, { filePath, data }) => ({ ...obj, [filePath]: JSON.stringify(data) }), - {}, - ); - - ranges.forEach(({ data: expectedData, range: targetRange }) => { - ranges - .filter(({ range: sourceRange }) => sourceRange !== targetRange) - .filter(({ range: sourceRange }) => !unsupported.includes(sourceRange)) - .forEach(({ filePath, range: sourceRange }) => { - it(`sets "${sourceRange}" semver ranges to the "${targetRange}" format`, async () => { - const program = getMockCommander(sources, '^((?!ignore).)*$'); - program.semverRange = targetRange; - mock(filesystem); - const noop = () => undefined; - const spyConsole = jest - .spyOn(console, 'log') - .mockImplementation(noop); - await run(program); - expect(readJsonSync(filePath)).toEqual(expectedData); - mock.restore(); - }); - }); - }); -}); diff --git a/src/set-semver-ranges.ts b/src/set-semver-ranges.ts deleted file mode 100644 index 1ff68d7e..00000000 --- a/src/set-semver-ranges.ts +++ /dev/null @@ -1,83 +0,0 @@ -import chalk from 'chalk'; -import { writeJson } from 'fs-extra'; -import _ = require('lodash'); -import { relative } from 'path'; -import * as semver from 'semver'; -import { - OPTION_INDENT, - OPTION_SEMVER_RANGE, - OPTION_SOURCES, - OPTIONS_DEV, - OPTIONS_FILTER_DEPENDENCIES, - OPTIONS_PEER, - OPTIONS_PROD, - RANGE_ANY, - RANGE_LOOSE, -} from './constants'; -import { collect } from './lib/collect'; -import { getDependencyTypes } from './lib/get-dependency-types'; -import { getIndent } from './lib/get-indent'; -import { getPackages } from './lib/get-packages'; -import { getDependencyFilter } from './lib/get-versions-by-name'; -import { getVersionNumber } from './lib/version'; -import { CommanderApi } from './typings'; - -export const run = async (program: CommanderApi) => { - 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) - .option( - OPTIONS_FILTER_DEPENDENCIES.spec, - OPTIONS_FILTER_DEPENDENCIES.description, - ) - .option(OPTION_INDENT.spec, OPTION_INDENT.description) - .parse(process.argv); - - const semverRange: string = - program.semverRange || OPTION_SEMVER_RANGE.default; - - const pkgs = getPackages(program); - const dependencyTypes = getDependencyTypes(program); - const indent = getIndent(program); - const dependencyFilter = getDependencyFilter(program.filter); - _(pkgs).each((pkg) => - _(dependencyTypes) - .map((property) => pkg.data[property]) - .filter(Boolean) - .each((dependencies) => { - _(dependencies).each((version, name) => { - if (!dependencyFilter(name)) { - return; - } - const versionNumber = getVersionNumber(version) - .split('.x') - .join('.0'); - 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, { spaces: indent })), - ); - - _.each(pkgs, (pkg) => { - console.log(chalk.blue(`./${relative('.', pkg.path)}`)); - }); -}; diff --git a/src/syncpack.spec.ts b/src/syncpack.spec.ts deleted file mode 100644 index b244add8..00000000 --- a/src/syncpack.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { sync } from 'glob'; -import { getMockCommander } from '../test/helpers'; -import { run } from './syncpack'; - -it('registers each command', () => { - const commands = [ - ...sync('bin-*.ts', { cwd: __dirname }), - ...sync('!*.spec.ts', { cwd: __dirname }), - ]; - const program = getMockCommander([]); - const spy = jest.spyOn(program, 'command'); - const commandNames = commands.map((basename) => - basename.replace(/bin\-|\.ts/g, ''), - ); - - run(program); - expect(commands.length).toBeGreaterThan(0); - expect(spy).toHaveBeenCalledTimes(commands.length); - - commandNames.forEach((name) => { - expect(spy.mock.calls).toEqual( - expect.arrayContaining([ - expect.arrayContaining([name, expect.any(String)]), - ]), - ); - }); -}); diff --git a/src/syncpack.ts b/src/syncpack.ts deleted file mode 100644 index 19b61edb..00000000 --- a/src/syncpack.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - FIX_MISMATCHES, - FORMAT, - LIST, - LIST_MISMATCHES, - SET_SEMVER_RANGES, - VERSION, -} from './constants'; -import { CommanderApi } from './typings'; - -export const run = (program: CommanderApi) => { - 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/src/typings.ts b/src/typings.ts deleted file mode 100644 index 7e2f02c8..00000000 --- a/src/typings.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CommanderStatic } from 'commander'; - -export interface IDictionary { - [key: string]: T; -} - -export type IManifestKey = - | 'dependencies' - | 'devDependencies' - | 'peerDependencies'; - -export interface IManifest { - dependencies: IDictionary; - devDependencies: IDictionary; - peerDependencies: IDictionary; - [otherProps: string]: string | IDictionary; -} - -export interface IFileDescriptor { - path: string; - data: object; -} - -export interface IManifestDescriptor { - path: string; - data: IManifest; -} - -// export interface IMockCommander { -// command: (...args: any[]) => jest.SpyInstance; -// option: (...args: any[]) => jest.SpyInstance; -// parse: (...args: any[]) => jest.SpyInstance; -// source: string[]; -// } - -export type CommanderApi = CommanderStatic; // | IMockCommander; diff --git a/test/fixtures/any.json b/test/fixtures/any.json deleted file mode 100644 index a5291077..00000000 --- a/test/fixtures/any.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": "*", - "commander": "*", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": "*", - "prettier": "*", - "rimraf": "*" - }, - "name": "foo", - "peerDependencies": { - "gulp": "*" - } - }, - { - "dependencies": { - "chalk": "*" - }, - "devDependencies": { - "jest": "*" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": "*" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/exact.json b/test/fixtures/exact.json deleted file mode 100644 index 9d848c36..00000000 --- a/test/fixtures/exact.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "dependencies": { - "chalk": "2.3.0", - "commander": "2.13.0", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": "22.1.3", - "prettier": "1.10.2", - "rimraf": "2.6.2" - }, - "name": "foo", - "peerDependencies": { - "gulp": "0.9.1" - } - }, - { - "dependencies": { - "chalk": "1.0.0", - "ignore": "2.0.0" - }, - "devDependencies": { - "jest": "22.1.4" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": "1.10.2" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] diff --git a/test/fixtures/gt.json b/test/fixtures/gt.json deleted file mode 100644 index dea67074..00000000 --- a/test/fixtures/gt.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": ">2.3.0", - "commander": ">2.13.0", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": ">22.1.3", - "prettier": ">1.10.2", - "rimraf": ">2.6.2" - }, - "name": "foo", - "peerDependencies": { - "gulp": ">0.9.1" - } - }, - { - "dependencies": { - "chalk": ">1.0.0" - }, - "devDependencies": { - "jest": ">22.1.4" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": ">1.10.2" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/gte.json b/test/fixtures/gte.json deleted file mode 100644 index 00aeb744..00000000 --- a/test/fixtures/gte.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": ">=2.3.0", - "commander": ">=2.13.0", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": ">=22.1.3", - "prettier": ">=1.10.2", - "rimraf": ">=2.6.2" - }, - "name": "foo", - "peerDependencies": { - "gulp": ">=0.9.1" - } - }, - { - "dependencies": { - "chalk": ">=1.0.0" - }, - "devDependencies": { - "jest": ">=22.1.4" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": ">=1.10.2" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/loose.json b/test/fixtures/loose.json deleted file mode 100644 index ee5ea62d..00000000 --- a/test/fixtures/loose.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": "2.x.x", - "commander": "2.x.x", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": "22.x.x", - "prettier": "1.x.x", - "rimraf": "2.x.x" - }, - "name": "foo", - "peerDependencies": { - "gulp": "0.9.x" - } - }, - { - "dependencies": { - "chalk": "1.x.x" - }, - "devDependencies": { - "jest": "22.x.x" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": "1.x.x" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/lt.json b/test/fixtures/lt.json deleted file mode 100644 index 0b9a6867..00000000 --- a/test/fixtures/lt.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": "<2.3.0", - "commander": "<2.13.0", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": "<22.1.3", - "prettier": "<1.10.2", - "rimraf": "<2.6.2" - }, - "name": "foo", - "peerDependencies": { - "gulp": "<0.9.1" - } - }, - { - "dependencies": { - "chalk": "<1.0.0" - }, - "devDependencies": { - "jest": "<22.1.4" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": "<1.10.2" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/lte.json b/test/fixtures/lte.json deleted file mode 100644 index 285da84d..00000000 --- a/test/fixtures/lte.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": "<=2.3.0", - "commander": "<=2.13.0", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": "<=22.1.3", - "prettier": "<=1.10.2", - "rimraf": "<=2.6.2" - }, - "name": "foo", - "peerDependencies": { - "gulp": "<=0.9.1" - } - }, - { - "dependencies": { - "chalk": "<=1.0.0" - }, - "devDependencies": { - "jest": "<=22.1.4" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": "<=1.10.2" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/minor.json b/test/fixtures/minor.json deleted file mode 100644 index f15ffad4..00000000 --- a/test/fixtures/minor.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": "^2.3.0", - "commander": "^2.13.0", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": "^22.1.3", - "prettier": "^1.10.2", - "rimraf": "^2.6.2" - }, - "name": "foo", - "peerDependencies": { - "gulp": "^0.9.1" - } - }, - { - "dependencies": { - "chalk": "^1.0.0" - }, - "devDependencies": { - "jest": "^22.1.4" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": "^1.10.2" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/patch.json b/test/fixtures/patch.json deleted file mode 100644 index a657684b..00000000 --- a/test/fixtures/patch.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "dependencies": { - "chalk": "~2.3.0", - "commander": "~2.13.0", - "ignore": "1.0.0" - }, - "devDependencies": { - "jest": "~22.1.3", - "prettier": "~1.10.2", - "rimraf": "~2.6.2" - }, - "name": "foo", - "peerDependencies": { - "gulp": "~0.9.1" - } - }, - { - "dependencies": { - "chalk": "~1.0.0" - }, - "devDependencies": { - "jest": "~22.1.4" - }, - "name": "bar" - }, - { - "devDependencies": { - "npm": "https://github.com/npm/npm.git", - "prettier": "~1.10.2" - }, - "name": "baz", - "peerDependencies": { - "gulp": "*" - } - } -] \ No newline at end of file diff --git a/test/fixtures/untidy.json b/test/fixtures/untidy.json deleted file mode 100644 index ae035321..00000000 --- a/test/fixtures/untidy.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "files": [ - "assets", - "dist" - ], - "bugs": { - "url": "https://github.com/JaneDoe/do-it/issues" - }, - "author": "Jane Doe ", - "devDependencies": { - "waldorf": "22.1.4", - "stroopwafel": "4.4.2" - }, - "scripts": { - "test": "jest", - "build": "tsc", - "lint": "tslint", - "format": "prettier" - }, - "version": "1.0.2", - "main": "do-it", - "license": "MIT", - "description": "Does the thing", - "homepage": "https://github.com/JaneDoe/do-it#readme", - "dependencies": { - "guybrush": "7.1.1", - "arnold": "5.0.0", - "dog": "2.13.0", - "mango": "2.3.0" - }, - "name": "do-it", - "repository": { - "url": "git://github.com/JaneDoe/do-it", - "type": "git" - }, - "keywords": [ - "those", - "whatsits", - "thing" - ], - "bin": { - "zoo": "dist/zoo.js", - "moose": "dist/moose.js", - "apple": "dist/apple.js" - }, - "peerDependencies": { - "jambalaya": "6.1.4", - "giftwrap": "0.1.2", - "zoolander": "1.4.25" - } -} \ No newline at end of file diff --git a/test/helpers.ts b/test/helpers.ts deleted file mode 100644 index bb7ea9a0..00000000 --- a/test/helpers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { readJsonSync } from 'fs-extra'; -import _ = require('lodash'); -import { CommanderApi } from '../src/typings'; - -const shuffle = (value: any): typeof value => - _.isArray(value) - ? _.shuffle(value) - : _.isObject(value) - ? shuffleObject(value) - : value; - -export const shuffleObject = (obj: object): object => - _(obj) - .entries() - .map(([key, value]) => [key, shuffle(value)]) - .shuffle() - .reduce((next, [key, value]) => ({ ...next, [key]: value }), {}); - -export const getFixture = (name: string, transform = (a: any | any[]) => a) => { - const fixturePath = `${__dirname}/fixtures/${name}.json`; - const data = transform(readJsonSync(fixturePath)); - const json = JSON.stringify(data, null, 2); - return { data, json }; -}; - -export const getMockCommander = ( - source: string[], - dependencyFilterString?: string, -) => { - const program = ({ - command: () => program, - filter: dependencyFilterString, - option: () => program, - parse: () => program, - source, - version: () => program, - } as unknown) as CommanderApi; - return program; -}; diff --git a/test/mock.ts b/test/mock.ts new file mode 100644 index 00000000..30acf6f8 --- /dev/null +++ b/test/mock.ts @@ -0,0 +1,19 @@ +import { SourceWrapper } from '../src/commands/lib/get-wrappers'; + +const toObject = (identifiers: string[]) => + identifiers.reduce((memo: any, dep) => { + const [name, version] = dep.split('@'); + memo[name] = version; + return memo; + }, {}); + +export const wrapper = (dirName: string, deps: string[], devDeps?: string[], peerDeps?: string[]): SourceWrapper => { + return { + contents: { + ...(deps ? { dependencies: toObject(deps) } : {}), + ...(devDeps ? { devDependencies: toObject(devDeps) } : {}), + ...(peerDeps ? { peerDependencies: toObject(peerDeps) } : {}), + }, + filePath: `/${dirName}/package.json`, + }; +}; diff --git a/test/setup.ts b/test/setup.ts deleted file mode 100644 index fd44bc53..00000000 --- a/test/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import 'expect-more-jest'; - -console.log('https://github.com/tschaub/mock-fs/issues/234'); diff --git a/tsconfig.json b/tsconfig.json index b9407d43..fe60e919 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "declaration": true, + "downlevelIteration": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "lib": ["es2017", "es6"], diff --git a/tslint.json b/tslint.json index 4ea4eaf5..4f886300 100644 --- a/tslint.json +++ b/tslint.json @@ -1,6 +1,8 @@ { "extends": "tslint:recommended", "rules": { + "forin": [false], + "interface-name": [false], "no-console": [false], "no-var-requires": [false], "quotemark": [true, "single"],