Skip to content

Commit

Permalink
feat: add canadyarn implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
lokshunhung committed Aug 21, 2020
1 parent a401bd5 commit 5ad7b14
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 0 deletions.
2 changes: 2 additions & 0 deletions bin/canadyarn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require("../dist/canadyarn");
92 changes: 92 additions & 0 deletions src/canadyarn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {parse} from "@yarnpkg/lockfile";
import {bold as cb} from "chalk";
import {existsSync, readFileSync, statSync} from "fs";
import {join} from "path";
import {checkYarnLock} from "./check-yarn-lock";
import {ERROR_UNKNOWN_EXIT_CODE, ExecutionError, MultipleVersionError} from "./error";
import type {DependencyName, MultipleVersionErrorMessage, ParseResult} from "./types";

function execute(): void {
const checkSingleVersionDependencies: DependencyName[] = (function () {
const filepath = join(process.cwd(), "package.json");
if (!(existsSync(filepath) && statSync(filepath).isFile())) {
throw new ExecutionError("cannot find package.json");
}
const packageJson = JSON.parse(readFileSync(filepath, {encoding: "utf8"}));
if (!("checkSingleVersionDependencies" in packageJson && Array.isArray(packageJson.checkSingleVersionDependencies))) {
throw new ExecutionError("package.json#checkSingleVersionDependencies must be an array");
}
return packageJson.checkSingleVersionDependencies;
})();
const lockfile: ParseResult = (function () {
const filepath = join(process.cwd(), "yarn.lock");
if (!(existsSync(filepath) && statSync(filepath).isFile())) {
throw new Error("[check-yarn-lock]: cannot find yarn.lock");
}
return parse(readFileSync(filepath, {encoding: "utf8"}));
})();
const errorMessages: MultipleVersionErrorMessage[] = checkYarnLock(checkSingleVersionDependencies, lockfile);
if (errorMessages.length > 0) {
throw new MultipleVersionError(errorMessages);
}
}

try {
execute();
} catch (error) {
const buffer: string[] = [];
let processExitCode: number;
buffer.push(`${cb.yellowBright("[check-yarn-lock-single-version]")}: `);
if (ExecutionError.isInstance(error)) {
const {exitCode, message} = error;
processExitCode = exitCode;
buffer.push("an execution error occurred ", cb.whiteBright(":("));
buffer.push("\n");
buffer.push(" > ", cb.whiteBright(message));
buffer.push("\n");
} else if (MultipleVersionError.isInstance(error)) {
const {exitCode, errorMessages} = error;
processExitCode = exitCode;
buffer.push(cb.whiteBright(`found ${cb.red(errorMessages.length)} modules with non-singular version resolutions`));
buffer.push("\n");
errorMessages.forEach(errorMessage => {
buffer.push(errorMessage);
buffer.push("\n");
});
buffer.push("\n");
buffer.push(Array(40).fill("-").join(""));
buffer.push("\n");
buffer.push("\n");
buffer.push(`The above list ${cb.whiteBright(`might contain ${cb.redBright("false")} positives`)} `);
buffer.push("if the resolution of dependency version range is removed after upgrading, ");
buffer.push(`but yarn does not remove the resolution from ${cb.whiteBright("yarn.lock")}.`);
buffer.push("\n");
buffer.push("\n");
buffer.push(`Try running "${cb.blueBright("$ yarn install")}" again in the workspace root to update ${cb.whiteBright("yarn.lock")}.`);
buffer.push("\n");
buffer.push("\n");
buffer.push(`Run "${cb.blueBright("$ yarn why <PACKAGE_NAME>")}" to find out where the duplicate dependencies come from.`);
buffer.push("\n");
buffer.push("\n");
buffer.push(`If you don't know what to do, `);
buffer.push(cb.whiteBright("ask the "), "🍁", cb.whiteBright("Canadian"), "🍁", cb.whiteBright(" guy"), " (he knows how to fix this) ", cb.whiteBright(":)"));
buffer.push("\n");
} else {
processExitCode = ERROR_UNKNOWN_EXIT_CODE;
buffer.push(cb.yellowBright("[check-yarn-lock-single-version]"));
buffer.push(": an unknown error occurred");
buffer.push("\n");
let message: string;
if (typeof error === "string") {
message = error;
} else if (typeof error === "object" && "message" in error && typeof error.message === "string") {
message = error.message;
}
if (message) {
buffer.push(" > ", cb.whiteBright(error.mess));
buffer.push("\n");
}
}
console.warn(buffer.join(""));
process.exit(processExitCode);
}
42 changes: 42 additions & 0 deletions src/check-yarn-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {bold as cb} from "chalk";
import {ExecutionError} from "./error";
import type {CheckableDependencies, DependencyName, MultipleVersionErrorMessage, ParseResult} from "./types";

export function checkYarnLock(checkSingleVersionDependencies: ReadonlyArray<DependencyName>, lockfile: Readonly<ParseResult>): MultipleVersionErrorMessage[] {
const checkableDeps: CheckableDependencies = (function () {
const checkableDeps = {};
checkSingleVersionDependencies.forEach(dep => {
checkableDeps[dep] = {};
});
return checkableDeps;
})();

if (lockfile.type !== "success") {
throw new ExecutionError("failed to parse yarn.lock");
}

const lockfileEntries = Object.entries(lockfile.object);

lockfileEntries.forEach(([qualifiedName, resolution]) => {
const dep = checkSingleVersionDependencies.find(dep => qualifiedName.startsWith(`${dep}@`));
if (dep !== undefined) {
checkableDeps[dep][resolution.version] = checkableDeps[dep][resolution.version] || [];
checkableDeps[dep][resolution.version].push(resolution); // only need resolution.version for now
}
});

const errorMessages: MultipleVersionErrorMessage[] = [];

Object.entries(checkableDeps).forEach(([dep, versionMap]) => {
const versions: string[] = Object.keys(versionMap);
if (versions.length !== 1) {
const buffer: string[] = [];
buffer.push(" > ", `${cb.red(versions.length)} versions of ${cb.blueBright(`"${dep}"`)} detected`);
buffer.push("\n");
buffer.push(" ", versions.join(", "));
errorMessages.push(buffer.join(""));
}
});

return errorMessages;
}
33 changes: 33 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {MultipleVersionErrorMessage} from "./types";

export const ERROR_EXECUTION_EXIT_CODE = 1;
export const ERROR_MULTIPLE_VERSION_EXIT_CODE = 2;
export const ERROR_UNKNOWN_EXIT_CODE = 3;

export class ExecutionError extends Error {
static isInstance(error: unknown): error is ExecutionError {
return typeof error === "object" && "__type__" in error && error["__type__"] === "ExecutionError";
}

readonly exitCode = ERROR_EXECUTION_EXIT_CODE;
private readonly __type__ = "ExecutionError";

constructor(message?: string) {
super(message);
}
}

export class MultipleVersionError extends Error {
static isInstance(error: unknown): error is MultipleVersionError {
return typeof error === "object" && "__type__" in error && error["__type__"] === "MultipleVersionError";
}

readonly exitCode = ERROR_MULTIPLE_VERSION_EXIT_CODE;
readonly errorMessages: MultipleVersionErrorMessage[];
private readonly __type__ = "MultipleVersionError";

constructor(errorMessages: MultipleVersionErrorMessage[]) {
super();
this.errorMessages = errorMessages;
}
}
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type DependencyName = ({__type__: "DependencyName"} & string) | string;

export type MultipleVersionErrorMessage = ({__type__: "MultipleVersionErrorMessage"} & string) | string;

export type QualifiedName = ({__type__: "QualifiedName"} & string) | string;

export type Version = ({__type__: "Version"} & string) | string;

export type DependencyResolution = {
version: string;
resolved: string;
integrity: string;
dependencies?: Record<string, string>;
};

export type ParseResult = {
type: "merge" | "success" | "conflict";
object: Record<QualifiedName, DependencyResolution>;
};

export type CheckableDependencies = Record<DependencyName, Record<Version, DependencyResolution[] | undefined>>;

0 comments on commit 5ad7b14

Please sign in to comment.