Skip to content

Commit

Permalink
Automatically Detect Package Manager (#17)
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Jacobson <ejacobson@wayfair.com>
  • Loading branch information
erj826 and Eric Jacobson authored May 17, 2022
1 parent 66a9281 commit 4a20c3a
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 22 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ This changelog format is largely based on [Keep A Changelog](https://keepachange

#### 🔨 Chores & Documentation

## [0.2.0] - 2022-05-16

#### 🚀 New Features & Enhancements

- Detect package manager based on lockfile

## [0.1.3] - 2022-05-03

#### 🔨 Chores & Documentation
Expand Down
1 change: 1 addition & 0 deletions ONE-VERSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ Although using the same specifier, the entries resolve to two different versions

- This library currently only operates on declared dependencies. That is the `dependencies`, `devDependencies`, and `peerDependencies` specified by a workspace - **not** any transitive dependencies.
- Resolutions are not yet taken into account.
- Package manager is selected based on the lockfile name in the root of the repo.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,12 @@ Add the following section to your package.json:
```json
{
"scripts": {
"one-version:check": "one-version check -p ${yarn | pnpm}"
"one-version:check": "one-version check"
}
}

```

The `-p` flag is not required if using `pnpm`.

Run `yarn one-version:check` or `pnpm run one-version:check`.

If the repo is compliant, the tool will print this message:
Expand All @@ -97,13 +95,20 @@ prettier

The behavior of `@wayfair/one-version` can be configured by a `one-version.config.json` at the root of the repository.

The only configuration this currently supports is an object of dependency `overrides`. This may be useful while performing major upgrades.
### Supported Options

#### overrides (optional, object)

```js
Overrides lets workspaces opt out of the one-version rule. This may be useful while performing major upgrades.

"overrides": {
dependency: {
versionSpecifier: [workspaceA, workspaceB]
### Examples

```json
{
"overrides": {
"dependency": {
"versionSpecifier": ["workspaceA", "workspaceB"]
}
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wayfair/one-version",
"version": "0.1.3",
"version": "0.2.0",
"description": "Opinionated Monorepo Dependency Management CLI",
"bin": "src/index.js",
"main": "src/index.js",
Expand Down
37 changes: 32 additions & 5 deletions src/__tests__/check.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,54 @@ const { check } = require("../check");
const {
NO_CHECK_API_ERROR,
FAILED_CHECK_ERROR,
UNABLE_TO_DETECT_PACKAGE_MANAGER_ERROR,
} = require("../shared/constants");

const packageManager = "fake-package-manager";
const otherPackageManager = "other-package-manager";

const mockGetConfig = () => ({ overrides: {} });
const mockGetConfig = () => ({
overrides: {},
packageManager: otherPackageManager,
});
const mockGetMissingPackageApi = () => {};

describe("one-version: check", () => {
it("throws if package manager specified does not have check api", () => {
it("throws if package manager is not supported", () => {
const mockDetectPackageManager = jest.fn();
mockDetectPackageManager.mockReturnValue("");

expect(() => {
check({
getPackageManager: mockDetectPackageManager,
getConfig: mockGetConfig,
getCheckApi: mockGetMissingPackageApi,
});
}).toThrow(`${UNABLE_TO_DETECT_PACKAGE_MANAGER_ERROR}`);
});

it("throws if package manager detected does not have check api", () => {
const mockDetectPackageManager = jest.fn();
mockDetectPackageManager.mockReturnValue(packageManager);

expect(() => {
check({
packageManager,
getPackageManager: mockDetectPackageManager,
getConfig: mockGetConfig,
getCheckApi: mockGetMissingPackageApi,
});
}).toThrow(`${NO_CHECK_API_ERROR} ${packageManager}`);
});

it("calls check api if found for package manager", () => {
const mockDetectPackageManager = jest.fn();
mockDetectPackageManager.mockReturnValue(packageManager);

const mockCheckApi = jest.fn();
mockCheckApi.mockReturnValue({ duplicateDependencies: [] });

check({
packageManager,
getPackageManager: mockDetectPackageManager,
getConfig: mockGetConfig,
getCheckApi: () => mockCheckApi,
});
Expand All @@ -34,12 +58,15 @@ describe("one-version: check", () => {
});

it("throws if check api finds duplicate dependencies", () => {
const mockDetectPackageManager = jest.fn();
mockDetectPackageManager.mockReturnValue(packageManager);

const mockCheckApi = jest.fn();
mockCheckApi.mockReturnValue({ duplicateDependencies: ["foo"] });

expect(() => {
check({
packageManager,
getPackageManager: mockDetectPackageManager,
getConfig: mockGetConfig,
getCheckApi: () => mockCheckApi,
prettify: () => {},
Expand Down
14 changes: 10 additions & 4 deletions src/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ Enforcing only one version of any direct dependency is specified in the repo.
Note: Currently enforces the specifications match exactly, i.e. `^17` != `17`.
*/
const chalk = require("chalk");
const { parseConfig } = require("./shared/util");
const { parseConfig, detectPackageManager } = require("./shared/util");
const { format } = require("./format-output");
const { checkYarn } = require("./yarn/check");
const { checkPnpm } = require("./pnpm/check");
const {
UNABLE_TO_DETECT_PACKAGE_MANAGER_ERROR,
FAILED_CHECK_ERROR,
NO_CHECK_API_ERROR,
} = require("./shared/constants");
Expand All @@ -22,15 +23,20 @@ const getCheckPackageApi = (packageManager) => {
};

const check = ({
packageManager,
getPackageManager = detectPackageManager,
getConfig = parseConfig,
getCheckApi = getCheckPackageApi,
prettify = format,
} = {}) => {
const { overrides } = getConfig();

const packageManager = getPackageManager();
if (!packageManager) {
throw new Error(UNABLE_TO_DETECT_PACKAGE_MANAGER_ERROR);
}

const checkApi = getCheckApi(packageManager);
if (checkApi) {
const { overrides } = getConfig();

const { duplicateDependencies } = checkApi({
overrides,
});
Expand Down
5 changes: 2 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ program
.description(
"Verify that only one version of each dependency exists in a monorepo"
)
.option("-p, --packageManager <type>", "package manager", "pnpm")
.action(({ packageManager }) => {
.action(() => {
try {
check({ packageManager });
check();
} catch (e) {
console.log(chalk.red(e.message));
process.exit(1);
Expand Down
25 changes: 25 additions & 0 deletions src/shared/__tests__/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ const {
getPackageDeps,
transformDependencies,
findDuplicateDependencies,
detectPackageManager,
} = require("../util");
const fs = require("fs");

jest.mock('fs', () => ({
...jest.requireActual('fs'),
existsSync: jest.fn(),
}));

const MOCK_TRANSFORMED_DEPENDENCIES = {
eslint: { "7.0.1": { direct: ["mock-app-a", "mock-app-b", "mock-app-c"] } },
Expand Down Expand Up @@ -118,4 +125,22 @@ describe("findDuplicateDependencies", () => {
],
]);
});

it("returns pnpm if pnpm-lock.yml exists", () => {
fs.existsSync.mockReturnValueOnce(true);
const packageManager = detectPackageManager({pnpm: "pnpm-lock.yaml",});
expect(packageManager).toBe('pnpm');
});

it("returns pnpm if yarn.lock exists", () => {
fs.existsSync.mockReturnValueOnce(true);
const packageManager = detectPackageManager({yarn: "yarn.lock"});
expect(packageManager).toBe('yarn');
});

it("returns empty if package manager is not detected", () => {
fs.existsSync.mockReturnValueOnce(true);
const packageManager = detectPackageManager({});
expect(packageManager).toBe('');
});
});
9 changes: 9 additions & 0 deletions src/shared/constants.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
const CONFIG_FILE = "oneversion.config.json";

const LOCKFILES = {
yarn: "yarn.lock",
pnpm: "pnpm-lock.yaml",
};

const DEPENDENCY_TYPES = {
DIRECT: "direct",
PEER: "peer",
DEV: "dev",
};

const UNABLE_TO_DETECT_PACKAGE_MANAGER_ERROR =
"Unable to detect a package manager. Try installing dependencies.";
const FAILED_CHECK_ERROR =
"More than one version of dependencies found. See above output.";
const NO_CHECK_API_ERROR = `'check' api not supported for package manager:`;

module.exports = {
CONFIG_FILE,
LOCKFILES,
DEPENDENCY_TYPES,
UNABLE_TO_DETECT_PACKAGE_MANAGER_ERROR,
FAILED_CHECK_ERROR,
NO_CHECK_API_ERROR,
};
15 changes: 14 additions & 1 deletion src/shared/util.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { readFileSync, existsSync } = require("fs");
const path = require("path");
const { DEPENDENCY_TYPES, CONFIG_FILE } = require("./constants");
const { DEPENDENCY_TYPES, CONFIG_FILE, LOCKFILES } = require("./constants");

/**
* Parse a config file if it exists
Expand Down Expand Up @@ -155,9 +155,22 @@ const findDuplicateDependencies = (dependencies, overrides) => {
.filter(([, versions]) => Object.keys(versions).length > 1);
};

/**
* Detect the package manager being used by the project
*/
const detectPackageManager = (lockfiles = LOCKFILES) => {
for (const [packageManager, lockfile] of Object.entries(lockfiles)) {
if (existsSync(lockfile)) {
return packageManager;
}
}
return "";
};

module.exports = {
parseConfig,
getPackageDeps,
transformDependencies,
findDuplicateDependencies,
detectPackageManager,
};

0 comments on commit 4a20c3a

Please sign in to comment.