Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add lint-semver-ranges command #56

Closed
wants to merge 8 commits into from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
coverage
dist
node_modules
.idea
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"syncpack-format": "dist/bin-format.js",
"syncpack-list-mismatches": "dist/bin-list-mismatches.js",
"syncpack-list": "dist/bin-list.js",
"syncpack-set-semver-ranges": "dist/bin-set-semver-ranges.js"
"syncpack-set-semver-ranges": "dist/bin-set-semver-ranges.js",
"syncpack-lint-semver-ranges": "dist/bin-lint-semver-ranges.js"
},
"bugs": "https://github.com/JamieMason/syncpack/issues",
"contributors": [
Expand Down
76 changes: 76 additions & 0 deletions src/bin-lint-semver-ranges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node

import chalk from 'chalk';
import { lintSemverRangesFromDisk } from './commands/lint-semver-ranges';
import { option } from './constants';
import { getConfig } from './lib/get-config';
import program = require('commander');

program.description(
`
Check whether dependency versions used within "dependencies", "devDependencies", and
"peerDependencies" follow a consistent format.`.replace(/^\n/, ''),
);

program.on('--help', () => {
console.log(chalk`
Examples:
{dim # uses defaults for resolving packages}
syncpack lint-semver-ranges
{dim # uses packages defined by --source when provided}
syncpack lint-semver-ranges --source {yellow "apps/*/package.json"}
{dim # multiple globs can be provided like this}
syncpack lint-semver-ranges --source {yellow "apps/*/package.json"} --source {yellow "core/*/package.json"}
{dim # uses dependencies regular expression defined by --filter when provided}
syncpack lint-semver-ranges --filter {yellow "typescript|tslint"}
{dim # use ~ range instead of default ""}
syncpack lint-semver-ranges --semver-range ~
{dim # use ~ range in "devDependencies"}
syncpack lint-semver-ranges --dev --semver-range ~
{dim # use ~ range in "devDependencies" and "peerDependencies"}
syncpack lint-semver-ranges --dev --peer --semver-range ~

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 Pnpm Workspaces, read {yellow packages} from {yellow pnpm-workspace.yaml} in the root of the project.
3. If using Yarn Workspaces, read {yellow workspaces} from {yellow package.json}.
4. If using Lerna, read {yellow packages} from {yellow lerna.json}.
5. 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}
Pnpm Workspaces {blue.underline https://pnpm.js.org/en/workspaces}
`);
});

program
.option(...option.source)
.option(...option.prod)
.option(...option.dev)
.option(...option.peer)
.option(...option.filter)
.option(...option.semverRange)
.parse(process.argv);

lintSemverRangesFromDisk(
getConfig({
dev: program.dev,
filter: program.filter,
peer: program.peer,
prod: program.prod,
semverRange: program.semverRange,
source: program.source,
}),
);
1 change: 1 addition & 0 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ program
.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')
.command('lint-semver-ranges', 'checks whether dependency versions comply with the given semver range format')
.parse(process.argv);
12 changes: 12 additions & 0 deletions src/commands/__snapshots__/lint-semver-ranges.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`lint-semver-ranges outputs all dependencies with incorrect versions 1`] = `
Array [
Array [
"✕ bar ^0.2.0 in dependencies of pkg2",
],
Array [
"✕ baz ~0.3.0 in dependencies of pkg2",
],
]
`;
46 changes: 46 additions & 0 deletions src/commands/lib/installations/get-installations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'expect-more-jest';
import { DEFAULT_CONFIG, DependencyType } from '../../../constants';
import { Source, SourceWrapper } from '../get-wrappers';
import { getInstallations } from './get-installations';
import { Installation } from './get-dependencies';


const filePath = '';
const sourceWrapper = (source: Source): SourceWrapper => ({contents: source, filePath})

const sources: Source[] = [
{ name: 'package1', dependencies: { chalk: '2.3.0' } },
{ name: 'package2', peerDependencies: { jest: '22.1.4' } },
{ name: 'package3', dependencies: { biggy: '0.1.0' } },
{ name: 'package4', devDependencies: { jest: '0.1.0' } }
];

const sourceWrappers = sources.map(source => sourceWrapper(source))

const installation = (source: SourceWrapper, dependencyName: string, dependencyVersion: string, dependencyType: DependencyType): Installation => ({
name: dependencyName,
source,
type:dependencyType,
version: dependencyVersion
})

describe('getInstallations', () => {
it('lists all installations', () => {
const iterator = getInstallations(sourceWrappers, DEFAULT_CONFIG);
expect(Array.from(iterator)).toEqual([
installation(sourceWrappers[0], 'chalk', '2.3.0', 'dependencies'),
installation(sourceWrappers[2], 'biggy', '0.1.0', 'dependencies'),
installation(sourceWrappers[3], 'jest', '0.1.0', 'devDependencies'),
installation(sourceWrappers[1], 'jest', '22.1.4', 'peerDependencies')
]);
});

it('lists all installations of packages matching the filter', () => {
const iterator = getInstallations(sourceWrappers, { ...DEFAULT_CONFIG, filter: 'jes|b' });
expect(Array.from(iterator)).toEqual([
installation(sourceWrappers[2], 'biggy', '0.1.0', 'dependencies'),
installation(sourceWrappers[3], 'jest', '0.1.0', 'devDependencies'),
installation(sourceWrappers[1], 'jest', '22.1.4', 'peerDependencies')
]);
});
});
19 changes: 19 additions & 0 deletions src/commands/lib/installations/get-installations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getDependencies, Installation, } from './get-dependencies';
import { SyncpackConfig } from '../../../constants';
import { SourceWrapper } from '../get-wrappers';
import { matchesFilter as createMatchesFilter } from '../matches-filter';

type Options = Pick<SyncpackConfig, 'dev' | 'peer' | 'prod' | 'filter'>;

export function* getInstallations(wrappers: SourceWrapper[], options: Options): Generator<Installation> {
const dependenciesIterator = getDependencies(wrappers, options);
const matchesFilter = createMatchesFilter(options);

for (const installedPackage of dependenciesIterator) {
if (matchesFilter(installedPackage)) {
for (const installation of installedPackage.installations) {
yield installation
}
}
}
}
68 changes: 68 additions & 0 deletions src/commands/lib/set-semver-range.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'expect-more-jest';
import { setSemverRange } from './set-semver-range';

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(([semverRange, expected]) => {
expect(setSemverRange({ semverRange })('<1.2.3')).toEqual(expected);
expect(setSemverRange({ semverRange })('<=1.2.3')).toEqual(expected);
expect(setSemverRange({ semverRange })('1.2.3')).toEqual(expected);
expect(setSemverRange({ semverRange })('~1.2.3')).toEqual(expected);
expect(setSemverRange({ semverRange })('^1.2.3')).toEqual(expected);
expect(setSemverRange({ semverRange })('>=1.2.3')).toEqual(expected);
expect(setSemverRange({ semverRange })('>1.2.3')).toEqual(expected);
expect(setSemverRange({ semverRange })('*')).toEqual('*');
expect(setSemverRange({ semverRange })('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({ semverRange: '' })(current)).toEqual('1.2.0');
expect(setSemverRange({ semverRange: '>' })(current)).toEqual('>1.2.0');
expect(setSemverRange({ semverRange: '>=' })(current)).toEqual('>=1.2.0');
expect(setSemverRange({ semverRange: '.x' })(current)).toEqual('1.x.x');
expect(setSemverRange({ semverRange: '<' })(current)).toEqual('<1.2.0');
expect(setSemverRange({ semverRange: '<=' })(current)).toEqual('<=1.2.0');
expect(setSemverRange({ semverRange: '^' })(current)).toEqual('^1.2.0');
expect(setSemverRange({ semverRange: '~' })(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({ semverRange: '' })(current)).toEqual('1.0.0');
expect(setSemverRange({ semverRange: '>' })(current)).toEqual('>1.0.0');
expect(setSemverRange({ semverRange: '>=' })(current)).toEqual('>=1.0.0');
expect(setSemverRange({ semverRange: '.x' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '<' })(current)).toEqual('<1.0.0');
expect(setSemverRange({ semverRange: '<=' })(current)).toEqual('<=1.0.0');
expect(setSemverRange({ semverRange: '^' })(current)).toEqual('^1.0.0');
expect(setSemverRange({ semverRange: '~' })(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({ semverRange: '' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '>' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '>=' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '.x' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '<' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '<=' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '^' })(current)).toEqual(current);
expect(setSemverRange({ semverRange: '~' })(current)).toEqual(current);
});
});
});
16 changes: 16 additions & 0 deletions src/commands/lib/set-semver-range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RANGE_LOOSE, SyncpackConfig } from '../../constants';
import { isLooseSemver, isSemver, isValidSemverRange } from './is-semver';

type Options = Pick<SyncpackConfig, 'semverRange'>;

export const setSemverRange = ({ semverRange }: Options) => (version: string): string => {
if (!isSemver(version) || !isValidSemverRange(semverRange)) {
return version;
}
const nextVersion = isLooseSemver(version) ? version.replace(/\.x/g, '.0') : version;
const from1stNumber = nextVersion.search(/[0-9]/);
const from1stDot = nextVersion.indexOf('.');
return semverRange === RANGE_LOOSE
? `${nextVersion.slice(from1stNumber, from1stDot)}.x.x`
: `${semverRange}${nextVersion.slice(from1stNumber)}`;
};
28 changes: 28 additions & 0 deletions src/commands/lint-semver-ranges.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'expect-more-jest';
import * as mock from '../../test/mock';
import { DEFAULT_CONFIG } from '../constants';
import * as api from './lint-semver-ranges';

describe('lint-semver-ranges', () => {
let lintSemverRanges: typeof api.lintSemverRanges;
let log: jest.Mock;

afterEach(() => {
jest.restoreAllMocks();
});

beforeEach(() => {
jest.mock('./lib/log', () => ({ log: jest.fn() }));
lintSemverRanges = require('./lint-semver-ranges').lintSemverRanges;
log = require('./lib/log').log;
});

it('outputs all dependencies with incorrect versions', () => {
const wrappers = [
mock.wrapper('a', ['foo@0.1.0'], [], [], { name: 'pkg1' }),
mock.wrapper('b', ['foo@0.2.0', 'bar@^0.2.0', 'baz@~0.3.0'], [], [], { name: 'pkg2' }),
];
lintSemverRanges(wrappers, { ...DEFAULT_CONFIG, dev: false, peer: false, prod: true });
expect(log.mock.calls).toMatchSnapshot();
});
});
44 changes: 44 additions & 0 deletions src/commands/lint-semver-ranges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import chalk from 'chalk';
import { SyncpackConfig } from '../constants';
import { getWrappers, SourceWrapper } from './lib/get-wrappers';
import { Installation } from './lib/installations/get-dependencies';
import { getInstallations } from './lib/installations/get-installations';
import { log } from './lib/log';
import { setSemverRange as createSetSemverRange } from './lib/set-semver-range';

type Options = Pick<SyncpackConfig, 'dev' | 'filter' | 'peer' | 'prod' | 'semverRange' | 'source'>;

export const lintSemverRanges = (
wrappers: SourceWrapper[],
options: Options,
): { installationsWithErrors: Installation[] } => {
const iterator = getInstallations(wrappers, options);
const setSemverRange = createSetSemverRange(options);

const installationsWithErrors: Installation[] = [];

for (const installation of iterator) {
const { name, type, version, source } = installation;
const dependencies = installation.source.contents[type];

if (dependencies) {
const currentVersion = dependencies[name];
const versionWithSelectedSemverRange = setSemverRange(version);
if (currentVersion !== versionWithSelectedSemverRange) {
log(chalk`{red ✕ ${name}} ${version} {dim in ${type} of ${source.contents.name}}`);
installationsWithErrors.push(installation);
}
}
}

return { installationsWithErrors };
};

export const lintSemverRangesFromDisk = (options: Options): void | never => {
const wrappers = getWrappers(options);
const { installationsWithErrors } = lintSemverRanges(wrappers, options);

if (installationsWithErrors.length > 0) {
process.exit(1);
}
};
Loading