diff --git a/README.md b/README.md index 282e719..919fbab 100644 --- a/README.md +++ b/README.md @@ -79,20 +79,23 @@ Usage: dependency-time-machine [options] Tool to automatically update dependencies one-by-one in the time order Options: - -p, --packageFile Path to package.json file (default: "package.json") - -u, --update Update package.json file with new versions - -is, --install-script Install with script (default: "npm install") - -ts, --test-script Test command (default: "npm test") - -i, --install Install with script - -t, --timeline Print timeline - -a, --auto Run in auto mode - -c, --cache Cache resolved dependencies - -cf, --cache-file Cache file (default: "./.dependency-time-machine/cache.json") - -e, --exclude Exclude dependency from update, separated by comma - -x, --exclude-file Exclude dependencies from file, one per line (default: "") - -r, --registry-url Registry url (default: "https://registry.npmjs.org") - -pi, --print-info Print info about the packages - -h, --help display help for command + -p, --packageFile Path to package.json file (default: "package.json") + -u, --update Update package.json file with new versions + -is, --install-script Install with script (default: "npm install") + -ts, --test-script Test command (default: "npm test") + -i, --install Install with script + -t, --timeline Print timeline + -a, --auto Run in auto mode + -c, --cache Cache resolved dependencies + -ans, --allow-non-semver Allow non-semver versions (compare with dates then, experimental) + -cf, --cache-file Cache file (default: "./.dependency-time-machine-cache.json") + -e, --exclude Exclude dependency from update, separated by comma + -r, --registry-url Registry url (default: "https://registry.npmjs.org") + -x, --exclude-file Exclude dependencies from file, one per line (default: "") + -shmn, --stop-if-higher-major-number Stop if higher major number + -shmnv, --stop-if-higher-minor-number Stop if higher minor or major number + -pi, --print-info Print info about the packages + -h, --help display help for command ``` ## License diff --git a/package-lock.json b/package-lock.json index 1ef497b..ce81d62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dependency-time-machine", - "version": "1.2.0", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dependency-time-machine", - "version": "1.2.0", + "version": "1.2.2", "license": "MIT", "dependencies": { "commander": "^7.2.0" diff --git a/package.json b/package.json index 38509c6..1b90d9e 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "dependency-time-machine", - "version": "1.2.1", + "version": "1.2.2", "description": "Tool to automatically update dependencies one-by-one in the time order", - "main": "./bin/index.js", + "main": "./bin/src/index.js", "repository": "https://github.com/pilotpirxie/dependency-time-machine.git", "author": "pilotpirxie <10637666+pilotpirxie@users.noreply.github.com>", "license": "MIT", @@ -18,7 +18,7 @@ "typescript": "^5.1.6" }, "bin": { - "dependency-time-machine": "./bin/index.js" + "dependency-time-machine": "./bin/src/index.js" }, "scripts": { "build": "tsc", diff --git a/src/exec/getRemoteDependencies.spec.ts b/src/exec/getRemoteDependencies.spec.ts index 0216329..afa7d31 100644 --- a/src/exec/getRemoteDependencies.spec.ts +++ b/src/exec/getRemoteDependencies.spec.ts @@ -22,6 +22,7 @@ describe("getRemoteDependencies", () => { cacheFilePath: "cache.json", excludedDependencies: [], registryUrl: "", + allowNonSemver: false, }); expect(existsSync).toHaveBeenCalledWith("cache.json"); @@ -42,6 +43,7 @@ describe("getRemoteDependencies", () => { cacheFilePath: "cache.json", excludedDependencies: [], registryUrl: "", + allowNonSemver: false, }); expect(existsSync).toHaveBeenCalledWith("cache.json"); @@ -57,6 +59,7 @@ describe("getRemoteDependencies", () => { cacheFilePath: "cache.json", excludedDependencies: [], registryUrl: "", + allowNonSemver: false, }); expect(dependencies).toBeDefined(); @@ -73,6 +76,7 @@ describe("getRemoteDependencies", () => { cacheFilePath: "cache.json", excludedDependencies: [], registryUrl: "https://registry.npmjs.org", + allowNonSemver: false, }); expect(dependencies).toBeDefined(); diff --git a/src/exec/getRemoteDependencies.ts b/src/exec/getRemoteDependencies.ts index d3fde79..7ae8f3b 100644 --- a/src/exec/getRemoteDependencies.ts +++ b/src/exec/getRemoteDependencies.ts @@ -11,12 +11,14 @@ export async function getRemoteDependencies({ cacheFilePath, excludedDependencies, registryUrl, + allowNonSemver, }: { localDependencies: LocalDependencies; cache: boolean; cacheFilePath: string; excludedDependencies: string[]; registryUrl: string; + allowNonSemver: boolean; }): Promise { try { const cacheExists = fs.existsSync(cacheFilePath); @@ -41,8 +43,8 @@ export async function getRemoteDependencies({ remoteDependencies.push(...newDependency); } - remoteDependencies = remoteDependencies.filter((dependency) => - isValidVersion(dependency.version), + remoteDependencies = remoteDependencies.filter( + (dependency) => isValidVersion(dependency.version) || allowNonSemver, ); const sortedRemoteDependencies = remoteDependencies diff --git a/src/exec/printDependenciesInfo.spec.ts b/src/exec/printDependenciesInfo.spec.ts index 7c43a88..70de5cd 100644 --- a/src/exec/printDependenciesInfo.spec.ts +++ b/src/exec/printDependenciesInfo.spec.ts @@ -19,12 +19,12 @@ describe("printDependenciesInfo", () => { { name: "react", version: "17.0.2", - published: new Date("2021-03-18T16:10:00.000Z"), + published: new Date("2021-03-18T16:10:00.000Z").toISOString(), }, { name: "react-dom", version: "17.0.2", - published: new Date("2021-03-18T16:00:00.000Z"), + published: new Date("2021-03-18T16:00:00.000Z").toISOString(), }, ], }); diff --git a/src/exec/updatePackageFile.spec.ts b/src/exec/updatePackageFile.spec.ts index 4d0c9e8..811ea3b 100644 --- a/src/exec/updatePackageFile.spec.ts +++ b/src/exec/updatePackageFile.spec.ts @@ -38,7 +38,7 @@ describe("updatePackageFile", () => { dependencyToUpdate: { name: "react", version: "17.0.2", - published: new Date(), + published: new Date().toISOString(), }, packageFilePath: "/", }); diff --git a/src/index.ts b/src/index.ts index a6d67a3..3649817 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,46 +1,52 @@ #!/usr/bin/env node -import { program } from "commander"; -import { run } from "./run"; +import {program} from "commander"; +import {run} from "./run"; program .name("dependency-time-machine") .description( - "Tool to automatically update dependencies one-by-one in the time order" + "Tool to automatically update dependencies one-by-one in the time order", ) .option( "-p, --packageFile ", "Path to package.json file", - "package.json" + "package.json", ) .option("-u, --update", "Update package.json file with new versions") .option( "-is, --install-script ", "Install with script", - "npm install" + "npm install", ) .option("-ts, --test-script ", "Test command", "npm test") .option("-i, --install", "Install with script") .option("-t, --timeline", "Print timeline") .option("-a, --auto", "Run in auto mode") .option("-c, --cache", "Cache resolved dependencies") + .option("-ans, --allow-non-semver", "Allow non-semver versions (compare with dates then, experimental)") .option( "-cf, --cache-file ", "Cache file", - "./.dependency-time-machine-cache.json" + "./.dependency-time-machine-cache.json", ) .option( "-e, --exclude ", - "Exclude dependency from update, separated by comma" + "Exclude dependency from update, separated by comma", ) .option( "-r, --registry-url ", "Registry url", - "https://registry.npmjs.org" + "https://registry.npmjs.org", ) .option( "-x, --exclude-file ", "Exclude dependencies from file, one per line", - "" + "", + ) + .option("-shmn, --stop-if-higher-major-number", "Stop if higher major number") + .option( + "-shmnv, --stop-if-higher-minor-number", + "Stop if higher minor or major number", ) .option("-pi, --print-info", "Print info about the packages") .action(run) diff --git a/src/run.ts b/src/run.ts index b1979c7..8be952b 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,14 +1,20 @@ import path from "path"; -import { getExcludedDependencies } from "./exec/getExcludedDependencies"; -import { getDependenciesFromPackageJson } from "./exec/getDependenciesFromPackageJson"; -import { getRemoteDependencies } from "./exec/getRemoteDependencies"; -import { Dependency } from "./types/Dependency"; -import { compareVersions } from "./util/semver"; -import { close } from "./exec/close"; -import { updatePackageFile } from "./exec/updatePackageFile"; -import { installDependency } from "./exec/installDependency"; -import { runTest } from "./exec/runTest"; -import { printDependenciesInfo } from "./exec/printDependenciesInfo"; +import {getExcludedDependencies} from "./exec/getExcludedDependencies"; +import {getDependenciesFromPackageJson} from "./exec/getDependenciesFromPackageJson"; +import {getRemoteDependencies} from "./exec/getRemoteDependencies"; +import {Dependency} from "./types/Dependency"; +import { + compareDates, + compareVersions, + isHigherMajorVersion, + isHigherMinorVersion, + isValidVersion, +} from "./util/semver"; +import {close} from "./exec/close"; +import {updatePackageFile} from "./exec/updatePackageFile"; +import {installDependency} from "./exec/installDependency"; +import {runTest} from "./exec/runTest"; +import {printDependenciesInfo} from "./exec/printDependenciesInfo"; export const run = async ({ packageFile, @@ -24,6 +30,9 @@ export const run = async ({ timeline, registryUrl, printInfo, + allowNonSemver, + stopIfHigherMajorNumber, + stopIfHigherMinorNumber, }: { packageFile: string; installScript: string; @@ -38,6 +47,9 @@ export const run = async ({ timeline: boolean | undefined; registryUrl: string; printInfo: boolean | undefined; + allowNonSemver: boolean | undefined; + stopIfHigherMajorNumber: boolean | undefined; + stopIfHigherMinorNumber: boolean | undefined; }) => { const currentDir = process.cwd(); const packageFilePath = path.join(currentDir, packageFile); @@ -56,36 +68,69 @@ export const run = async ({ cacheFilePath: path.join(currentDir, cacheFile), excludedDependencies, registryUrl, + allowNonSemver: !!allowNonSemver, }); let previousDependency: Dependency | null = null; let dependencyToUpdate = null; const timelineToPrint: Dependency[] = []; + for (let i = 0; i < sortedRemoteDependencies.length; i++) { const dependency = sortedRemoteDependencies[i]; - if ( - compareVersions( - dependency.version, - localDependencies[dependency.name], - ) > 0 - ) { + + const dependencyToCompare = sortedRemoteDependencies.find( + (d) => d.name === dependency.name, + ); + + if (!dependencyToCompare) { + continue; + } + + const compareAgainstLocalDependency = !isValidVersion( + dependency.version, + localDependencies[dependency.name], + ) + ? compareDates(dependency.published, dependencyToCompare.published) > 0 + : compareVersions( + dependency.version, + localDependencies[dependency.name], + ) > 0; + + if (compareAgainstLocalDependency) { if (dependencyToUpdate === null) { previousDependency = { name: dependency.name, version: localDependencies[dependency.name], - published: new Date(), + published: new Date().toISOString(), }; dependencyToUpdate = dependency; timelineToPrint.push(...sortedRemoteDependencies.slice(i)); } - if (dependencyToUpdate.name !== dependency.name) { + const versioningStop = + (stopIfHigherMajorNumber && + isHigherMajorVersion( + dependency.version, + dependencyToUpdate.version, + )) || + (stopIfHigherMinorNumber && + isHigherMinorVersion( + dependency.version, + dependencyToUpdate.version, + )); + + if (dependencyToUpdate.name !== dependency.name || versioningStop) { break; } - if ( - compareVersions(dependency.version, dependencyToUpdate.version) > 0 - ) { + const compareAgainstDependencyToUpdate = !isValidVersion( + dependency.version, + dependencyToUpdate.version, + ) + ? compareDates(dependency.published, dependencyToUpdate.published) > 0 + : compareVersions(dependency.version, dependencyToUpdate.version) > 0; + + if (compareAgainstDependencyToUpdate) { dependencyToUpdate = dependency; } } diff --git a/src/types/Dependency.ts b/src/types/Dependency.ts index 69d4c2c..323fec6 100644 --- a/src/types/Dependency.ts +++ b/src/types/Dependency.ts @@ -1,5 +1,5 @@ export type Dependency = { name: string; version: string; - published: Date; + published: string; }; diff --git a/src/util/httpDependencyResolver.ts b/src/util/httpDependencyResolver.ts index f56a976..b907756 100644 --- a/src/util/httpDependencyResolver.ts +++ b/src/util/httpDependencyResolver.ts @@ -27,7 +27,7 @@ function fetchJson(url: string): Promise { export default async function httpDependencyResolver( name: string, - registryUrl: string + registryUrl: string, ): Promise { const url = `${registryUrl}/${name}`; try { @@ -39,7 +39,7 @@ export default async function httpDependencyResolver( dependencyVersionsWithPublishedDate.push({ name, version, - published: new Date(published), + published: new Date(published).toISOString(), }); } diff --git a/src/util/semver.spec.ts b/src/util/semver.spec.ts index c7bf04d..31fb039 100644 --- a/src/util/semver.spec.ts +++ b/src/util/semver.spec.ts @@ -1,4 +1,11 @@ -import { compareVersions, isValidVersion, parseVersion } from "./semver"; +import { + compareDates, + compareVersions, + isHigherMajorVersion, + isHigherMinorVersion, + isValidVersion, + parseVersion, +} from "./semver"; describe("semver", () => { test("isValidVersion", () => { @@ -13,6 +20,29 @@ describe("semver", () => { expect(isValidVersion("1.2.3+build.1.")).toBe(false); }); + test("isValidVersion multiple", () => { + expect(isValidVersion("1.2.3", "1.2.3")).toBe(true); + expect(isValidVersion("1.2.3", "1.2.3-rc.1")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3-rc.1+build.1")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3+build.1")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3+build.1.2")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3+build.1.2.3")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3+build.")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3+build")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3+build.1.")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3", "1.2.3")).toBe(true); + expect(isValidVersion("1.2.3", "1.2.3", "1.2.3-rc.1")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3", "1.2.3-rc.1+build.1")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3", "1.2.3+build.1")).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3", "1.2.3+build.1.2")).toBe(false); + expect( + isValidVersion("1.2.3", "1.2.3", "1.2.3+build.1.2.3", "1.2.3", "1.2.3."), + ).toBe(false); + expect(isValidVersion("1.2.3", "1.2.3", "1.2.3", "1.2.3+build.")).toBe( + false, + ); + }); + test("parseVersion", () => { expect(parseVersion("1.2.3")).toEqual([1, 2, 3]); expect(parseVersion("v1.2.3")).toEqual([1, 2, 3]); @@ -30,4 +60,45 @@ describe("semver", () => { expect(compareVersions("1.2.3", "1.1.3")).toBe(1); expect(compareVersions("1.2.3", "0.2.3")).toBe(1); }); + + test("compareDates", () => { + expect( + compareDates("2021-03-18T16:10:00.000Z", "2021-03-18T16:10:00.000Z"), + ).toBe(0); + expect( + compareDates("2021-03-18T16:10:00.000Z", "2021-03-18T16:10:01.000Z"), + ).toBe(-1); + expect( + compareDates("2021-03-18T16:10:00.000Z", "2021-03-18T16:10:00.001Z"), + ).toBe(-1); + expect( + compareDates("2021-03-18T16:10:00.000Z", "2021-03-18T16:10:00.000Z"), + ).toBe(0); + expect( + compareDates("2021-03-18T16:10:00.000Z", "2021-03-18T16:09:59.999Z"), + ).toBe(1); + expect( + compareDates("2021-03-18T16:10:00.000Z", "2021-03-18T16:09:59.000Z"), + ).toBe(1); + }); + + test("isHigherMajorVersion", () => { + expect(isHigherMajorVersion("1.2.3", "1.2.3")).toBe(false); + expect(isHigherMajorVersion("1.2.3", "1.2.4")).toBe(false); + expect(isHigherMajorVersion("1.2.3", "1.3.3")).toBe(false); + expect(isHigherMajorVersion("1.2.3", "2.2.3")).toBe(false); + expect(isHigherMajorVersion("1.2.3", "1.2.2")).toBe(false); + expect(isHigherMajorVersion("1.2.3", "1.1.3")).toBe(false); + expect(isHigherMajorVersion("1.2.3", "0.2.3")).toBe(true); + }); + + test("isHigherMinorVersion", () => { + expect(isHigherMinorVersion("1.2.3", "1.2.3")).toBe(false); + expect(isHigherMinorVersion("1.2.3", "1.2.4")).toBe(false); + expect(isHigherMinorVersion("1.2.3", "1.3.3")).toBe(false); + expect(isHigherMinorVersion("1.2.3", "2.2.3")).toBe(false); + expect(isHigherMinorVersion("1.2.3", "1.2.2")).toBe(false); + expect(isHigherMinorVersion("1.2.3", "1.1.3")).toBe(true); + expect(isHigherMinorVersion("1.2.3", "0.2.3")).toBe(true); + }); }); diff --git a/src/util/semver.ts b/src/util/semver.ts index 0216fc9..0e32635 100644 --- a/src/util/semver.ts +++ b/src/util/semver.ts @@ -1,12 +1,12 @@ -const versionRegex = /^\D*(\d+)\.(\d+)\.(\d+)\D*$/; +// const strictVersionRegex = /^\D*(\d+)\.(\d+)\.(\d+)\D*$/; const strictVersionRegex = /^\D*(\d+)\.(\d+)\.(\d+)$/; -export const isValidVersion = (version: string): boolean => { - return Boolean(version.match(strictVersionRegex)); +export const isValidVersion = (...version: string[]): boolean => { + return version.every((v) => strictVersionRegex.test(v)); }; export const parseVersion = ( - version: string + version: string, ): [number, number, number, string?] => { const match = version.match(strictVersionRegex); @@ -27,3 +27,30 @@ export const compareVersions = (a: string, b: string): number => { return 0; }; + +export const compareDates = (a: string, b: string): number => { + const dateA = new Date(a); + const dateB = new Date(b); + + const result = dateA.getTime() - dateB.getTime(); + + if (result === 0) return 0; + if (result > 0) return 1; + if (result < 0) return -1; + + return 0; +}; + +export const isHigherMajorVersion = (a: string, b: string): boolean => { + const [majorA] = parseVersion(a); + const [majorB] = parseVersion(b); + + return majorA > majorB; +}; + +export const isHigherMinorVersion = (a: string, b: string): boolean => { + const [majorA, minorA] = parseVersion(a); + const [majorB, minorB] = parseVersion(b); + + return isHigherMajorVersion(a, b) || minorA > minorB; +};