-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a401bd5
commit 5ad7b14
Showing
5 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#!/usr/bin/env node | ||
require("../dist/canadyarn"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>>; |