From a3143a80d1e5df66c58c837022038d92dd836811 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 31 Oct 2023 11:18:25 -0700 Subject: [PATCH] Normalizer versioning fix (#1785) * Normalizer versioning fix * Fix error * add logic to find newest service * Add util-semver package * Move logic to normalizeServices --- .changeset/early-olives-explode.md | 5 +++ package-lock.json | 17 +++++++ packages/fcl/package.json | 3 +- packages/fcl/src/current-user/build-user.js | 19 ++++---- .../fcl/src/current-user/merge-services.js | 2 - .../fcl/src/current-user/service-of-type.js | 13 +++++- .../src/current-user/service-of-type.test.js | 40 +++++++++++++++++ .../src/normalizers/service/account-proof.js | 6 ++- .../src/normalizers/service/authn-refresh.js | 6 ++- packages/fcl/src/normalizers/service/authn.js | 28 +++++++----- packages/fcl/src/normalizers/service/authz.js | 32 +++++++------ .../normalizers/service/back-channel-rpc.js | 20 +++++---- .../service/composite-signature.js | 16 ++++--- packages/fcl/src/normalizers/service/frame.js | 20 +++++---- .../fcl/src/normalizers/service/local-view.js | 20 +++++---- .../normalizers/service/polling-response.js | 20 +++++---- .../fcl/src/normalizers/service/pre-authz.js | 32 +++++++------ .../fcl/src/normalizers/service/service.js | 4 +- .../src/normalizers/service/user-signature.js | 6 ++- packages/util-semver/.babelrc | 7 +++ packages/util-semver/package.json | 32 +++++++++++++ .../util-semver/src/compare-identifiers.js | 36 +++++++++++++++ packages/util-semver/src/compare.js | 45 +++++++++++++++++++ packages/util-semver/src/compare.test.js | 33 ++++++++++++++ packages/util-semver/src/index.js | 1 + 25 files changed, 367 insertions(+), 96 deletions(-) create mode 100644 .changeset/early-olives-explode.md create mode 100644 packages/fcl/src/current-user/service-of-type.test.js create mode 100644 packages/util-semver/.babelrc create mode 100644 packages/util-semver/package.json create mode 100644 packages/util-semver/src/compare-identifiers.js create mode 100644 packages/util-semver/src/compare.js create mode 100644 packages/util-semver/src/compare.test.js create mode 100644 packages/util-semver/src/index.js diff --git a/.changeset/early-olives-explode.md b/.changeset/early-olives-explode.md new file mode 100644 index 000000000..be6e51f5b --- /dev/null +++ b/.changeset/early-olives-explode.md @@ -0,0 +1,5 @@ +--- +"@onflow/fcl": patch +--- + +Fix version normalization of services in FCL diff --git a/package-lock.json b/package-lock.json index 7e45b3aef..33acc1fb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4599,6 +4599,10 @@ "resolved": "packages/util-logger", "link": true }, + "node_modules/@onflow/util-semver": { + "resolved": "packages/util-semver", + "link": true + }, "node_modules/@onflow/util-template": { "resolved": "packages/util-template", "link": true @@ -19066,6 +19070,7 @@ "@onflow/util-address": "^1.1.0", "@onflow/util-invariant": "^1.1.0", "@onflow/util-logger": "^1.2.2", + "@onflow/util-semver": "^1.0.0", "@onflow/util-template": "^1.1.0", "@onflow/util-uid": "^1.1.0", "cross-fetch": "^3.1.6" @@ -20195,6 +20200,18 @@ } } }, + "packages/util-semver": { + "name": "@onflow/util-semver", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.18.6" + }, + "devDependencies": { + "@onflow/fcl-bundle": "^1.3.1", + "jest": "^29.5.0" + } + }, "packages/util-template": { "name": "@onflow/util-template", "version": "1.1.0", diff --git a/packages/fcl/package.json b/packages/fcl/package.json index baeb34553..8080bbe81 100644 --- a/packages/fcl/package.json +++ b/packages/fcl/package.json @@ -67,14 +67,15 @@ "@onflow/util-address": "^1.1.0", "@onflow/util-invariant": "^1.1.0", "@onflow/util-logger": "^1.2.2", + "@onflow/util-semver": "^1.0.0", "@onflow/util-template": "^1.1.0", "@onflow/util-uid": "^1.1.0", "cross-fetch": "^3.1.6" }, "peerDependencies": { "@react-native-async-storage/async-storage": "^1.13.0", - "expo-web-browser": "^12.0.0", "expo-linking": "^4.0.0", + "expo-web-browser": "^12.0.0", "react": "^17.0.0 || ^18.0.0", "react-native": "^0.0.0-0 || 0.60 - 0.72 || 1000.0.0" }, diff --git a/packages/fcl/src/current-user/build-user.js b/packages/fcl/src/current-user/build-user.js index f0b9414e4..516e369f0 100644 --- a/packages/fcl/src/current-user/build-user.js +++ b/packages/fcl/src/current-user/build-user.js @@ -3,7 +3,11 @@ import * as rlp from "@onflow/rlp" import {fetchServices} from "./fetch-services" import {mergeServices} from "./merge-services" import {USER_PRAGMA} from "../normalizers/service/__vsn" -import {normalizeService} from "../normalizers/service/service" +import { + normalizeService, + normalizeServices, +} from "../normalizers/service/service" +import {serviceOfType} from "./service-of-type" function deriveCompositeId(authn) { return rlp @@ -20,19 +24,14 @@ function normalizeData(data) { return data } -function findService(type, services) { - return services.find(d => d.type === type) -} - export async function buildUser(data) { data = normalizeData(data) - var services = mergeServices( - data.services || [], - await fetchServices(data.hks, data.code) - ).map(service => normalizeService(service, data)) + var services = normalizeServices( + mergeServices(data.services || [], await fetchServices(data.hks, data.code)) + ) - const authn = findService("authn", services) + const authn = serviceOfType(services, "authn") return { ...USER_PRAGMA, diff --git a/packages/fcl/src/current-user/merge-services.js b/packages/fcl/src/current-user/merge-services.js index 6d1f345d9..9a705c283 100644 --- a/packages/fcl/src/current-user/merge-services.js +++ b/packages/fcl/src/current-user/merge-services.js @@ -1,5 +1,3 @@ -import {withPrefix} from "@onflow/util-address" - export function mergeServices(sx1 = [], sx2 = []) { // TODO: Make this smarter return [...sx1, ...sx2] diff --git a/packages/fcl/src/current-user/service-of-type.js b/packages/fcl/src/current-user/service-of-type.js index 6347236d8..cd1b2ac4e 100644 --- a/packages/fcl/src/current-user/service-of-type.js +++ b/packages/fcl/src/current-user/service-of-type.js @@ -1,3 +1,14 @@ +import * as semver from "@onflow/util-semver" + export function serviceOfType(services = [], type) { - return services.find(service => service.type === type) + // Find the greatest version of the service type + return services.reduce( + (mostRecent, service) => + service.type === type + ? !mostRecent || semver.compare(service.f_vsn, mostRecent.f_vsn) > 0 + ? service + : mostRecent + : mostRecent, + null + ) } diff --git a/packages/fcl/src/current-user/service-of-type.test.js b/packages/fcl/src/current-user/service-of-type.test.js new file mode 100644 index 000000000..0feb567ce --- /dev/null +++ b/packages/fcl/src/current-user/service-of-type.test.js @@ -0,0 +1,40 @@ +import {serviceOfType} from "./service-of-type" + +describe("service-of-type", () => { + it("should choose the most recent version of a service", () => { + const services = [ + { + type: "authn", + f_vsn: "1.0.0", + }, + { + type: "authn", + f_vsn: "2.0.0", + }, + ] + + const service = serviceOfType(services, "authn") + + expect(service).toEqual({ + type: "authn", + f_vsn: "2.0.0", + }) + }) + + it("should return null if no service of type exists", () => { + const services = [ + { + type: "authn", + f_vsn: "1.0.0", + }, + { + type: "authn", + f_vsn: "2.0.0", + }, + ] + + const service = serviceOfType(services, "non-existent") + + expect(service).toBe(null) + }) +}) diff --git a/packages/fcl/src/normalizers/service/account-proof.js b/packages/fcl/src/normalizers/service/account-proof.js index 4f4264dc3..dc1efca59 100644 --- a/packages/fcl/src/normalizers/service/account-proof.js +++ b/packages/fcl/src/normalizers/service/account-proof.js @@ -15,11 +15,15 @@ export function normalizeAccountProof(service) { if (service == null) return null + if (!service["f_vsn"]) { + throw new Error(`FCL Normalizer Error: Invalid account-proof service`) + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - throw new Error(`FCL Normalizer Error: Invalid account-proof service`) + return null } } diff --git a/packages/fcl/src/normalizers/service/authn-refresh.js b/packages/fcl/src/normalizers/service/authn-refresh.js index b345405b8..37b7d9d6d 100644 --- a/packages/fcl/src/normalizers/service/authn-refresh.js +++ b/packages/fcl/src/normalizers/service/authn-refresh.js @@ -12,11 +12,15 @@ export function normalizeAuthnRefresh(service) { if (service == null) return null + if (!service["f_vsn"]) { + throw new Error("Invalid authn-refresh service") + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - throw new Error("Invalid authn-refresh service") + return null } } diff --git a/packages/fcl/src/normalizers/service/authn.js b/packages/fcl/src/normalizers/service/authn.js index adcc04b38..eaf60f29a 100644 --- a/packages/fcl/src/normalizers/service/authn.js +++ b/packages/fcl/src/normalizers/service/authn.js @@ -21,22 +21,26 @@ import {SERVICE_PRAGMA} from "./__vsn" export function normalizeAuthn(service) { if (service == null) return null + if (!service["f_vsn"]) { + return { + ...SERVICE_PRAGMA, + type: service.type, + uid: service.id, + endpoint: service.authn, + id: service.pid, + provider: { + address: withPrefix(service.addr), + name: service.name, + icon: service.icon, + }, + } + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - return { - ...SERVICE_PRAGMA, - type: service.type, - uid: service.id, - endpoint: service.authn, - id: service.pid, - provider: { - address: withPrefix(service.addr), - name: service.name, - icon: service.icon, - }, - } + return null } } diff --git a/packages/fcl/src/normalizers/service/authz.js b/packages/fcl/src/normalizers/service/authz.js index 700bc0d90..bf0a8552b 100644 --- a/packages/fcl/src/normalizers/service/authz.js +++ b/packages/fcl/src/normalizers/service/authz.js @@ -18,24 +18,28 @@ import {SERVICE_PRAGMA, IDENTITY_PRAGMA} from "./__vsn" export function normalizeAuthz(service) { if (service == null) return null + if (!service["f_vsn"]) { + return { + ...SERVICE_PRAGMA, + type: service.type, + uid: service.id, + endpoint: service.endpoint, + method: service.method, + identity: { + ...IDENTITY_PRAGMA, + address: withPrefix(service.addr), + keyId: service.keyId, + }, + params: service.params, + data: service.data, + } + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - return { - ...SERVICE_PRAGMA, - type: service.type, - uid: service.id, - endpoint: service.endpoint, - method: service.method, - identity: { - ...IDENTITY_PRAGMA, - address: withPrefix(service.addr), - keyId: service.keyId, - }, - params: service.params, - data: service.data, - } + return null } } diff --git a/packages/fcl/src/normalizers/service/back-channel-rpc.js b/packages/fcl/src/normalizers/service/back-channel-rpc.js index 64d827983..9f8467104 100644 --- a/packages/fcl/src/normalizers/service/back-channel-rpc.js +++ b/packages/fcl/src/normalizers/service/back-channel-rpc.js @@ -12,18 +12,22 @@ import {SERVICE_PRAGMA} from "./__vsn" export function normalizeBackChannelRpc(service) { if (service == null) return null + if (!service["f_vsn"]) { + return { + ...SERVICE_PRAGMA, + type: "back-channel-rpc", + endpoint: service.endpoint, + method: service.method, + params: service.params || {}, + data: service.data || {}, + } + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - return { - ...SERVICE_PRAGMA, - type: "back-channel-rpc", - endpoint: service.endpoint, - method: service.method, - params: service.params || {}, - data: service.data || {}, - } + return null } } diff --git a/packages/fcl/src/normalizers/service/composite-signature.js b/packages/fcl/src/normalizers/service/composite-signature.js index 7681c858d..47aea2a25 100644 --- a/packages/fcl/src/normalizers/service/composite-signature.js +++ b/packages/fcl/src/normalizers/service/composite-signature.js @@ -11,16 +11,20 @@ import {sansPrefix} from "@onflow/util-address" export function normalizeCompositeSignature(resp) { if (resp == null) return null + if (!resp["f_vsn"]) { + return { + ...COMPOSITE_SIGNATURE_PRAGMA, + addr: sansPrefix(resp.addr || resp.address), + signature: resp.signature || resp.sig, + keyId: resp.keyId, + } + } + switch (resp["f_vsn"]) { case "1.0.0": return resp default: - return { - ...COMPOSITE_SIGNATURE_PRAGMA, - addr: sansPrefix(resp.addr || resp.address), - signature: resp.signature || resp.sig, - keyId: resp.keyId, - } + return null } } diff --git a/packages/fcl/src/normalizers/service/frame.js b/packages/fcl/src/normalizers/service/frame.js index 6a63b8dd3..076d80b7e 100644 --- a/packages/fcl/src/normalizers/service/frame.js +++ b/packages/fcl/src/normalizers/service/frame.js @@ -11,18 +11,22 @@ import {SERVICE_PRAGMA} from "./__vsn" export function normalizeFrame(service) { if (service == null) return null + if (!service["f_vsn"]) { + return { + old: service, + ...SERVICE_PRAGMA, + type: "frame", + endpoint: service.endpoint, + params: service.params || {}, + data: service.data || {}, + } + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - return { - old: service, - ...SERVICE_PRAGMA, - type: "frame", - endpoint: service.endpoint, - params: service.params || {}, - data: service.data || {}, - } + return null } } diff --git a/packages/fcl/src/normalizers/service/local-view.js b/packages/fcl/src/normalizers/service/local-view.js index f34df1ccb..ce9f37432 100644 --- a/packages/fcl/src/normalizers/service/local-view.js +++ b/packages/fcl/src/normalizers/service/local-view.js @@ -15,18 +15,22 @@ export function normalizeLocalView(resp) { resp = {...resp, type: "local-view", method: "VIEW/IFRAME"} } + if (!resp["f_vsn"]) { + return { + ...SERVICE_PRAGMA, + type: resp.type || "local-view", + method: resp.method, + endpoint: resp.endpoint, + data: resp.data || {}, + params: resp.params || {}, + } + } + switch (resp["f_vsn"]) { case "1.0.0": return resp default: - return { - ...SERVICE_PRAGMA, - type: resp.type || "local-view", - method: resp.method, - endpoint: resp.endpoint, - data: resp.data || {}, - params: resp.params || {}, - } + return null } } diff --git a/packages/fcl/src/normalizers/service/polling-response.js b/packages/fcl/src/normalizers/service/polling-response.js index 861865d52..5e560de4e 100644 --- a/packages/fcl/src/normalizers/service/polling-response.js +++ b/packages/fcl/src/normalizers/service/polling-response.js @@ -14,18 +14,22 @@ import {normalizeFrame} from "./frame" export function normalizePollingResponse(resp) { if (resp == null) return null + if (!resp["f_vsn"]) { + return { + ...POLLING_RESPONSE_PRAGMA, + status: resp.status ?? "APPROVED", + reason: resp.reason ?? null, + data: resp.compositeSignature || resp.data || {...resp} || {}, + updates: normalizeBackChannelRpc(resp.authorizationUpdates), + local: normalizeFrame((resp.local || [])[0]), + } + } + switch (resp["f_vsn"]) { case "1.0.0": return resp default: - return { - ...POLLING_RESPONSE_PRAGMA, - status: resp.status ?? "APPROVED", - reason: resp.reason ?? null, - data: resp.compositeSignature || resp.data || {...resp} || {}, - updates: normalizeBackChannelRpc(resp.authorizationUpdates), - local: normalizeFrame((resp.local || [])[0]), - } + return null } } diff --git a/packages/fcl/src/normalizers/service/pre-authz.js b/packages/fcl/src/normalizers/service/pre-authz.js index f0bc2eb23..ca0c854fa 100644 --- a/packages/fcl/src/normalizers/service/pre-authz.js +++ b/packages/fcl/src/normalizers/service/pre-authz.js @@ -18,24 +18,28 @@ import {SERVICE_PRAGMA, IDENTITY_PRAGMA} from "./__vsn" export function normalizePreAuthz(service) { if (service == null) return null + if (!service["f_vsn"]) { + return { + ...SERVICE_PRAGMA, + type: service.type, + uid: service.id, + endpoint: service.endpoint, + method: service.method, + identity: { + ...IDENTITY_PRAGMA, + address: withPrefix(service.addr), + keyId: service.keyId, + }, + params: service.params, + data: service.data, + } + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - return { - ...SERVICE_PRAGMA, - type: service.type, - uid: service.id, - endpoint: service.endpoint, - method: service.method, - identity: { - ...IDENTITY_PRAGMA, - address: withPrefix(service.addr), - keyId: service.keyId, - }, - params: service.params, - data: service.data, - } + return null } } diff --git a/packages/fcl/src/normalizers/service/service.js b/packages/fcl/src/normalizers/service/service.js index 29999ba43..0d0673106 100644 --- a/packages/fcl/src/normalizers/service/service.js +++ b/packages/fcl/src/normalizers/service/service.js @@ -10,7 +10,9 @@ import {normalizeAccountProof} from "./account-proof" import {normalizeAuthnRefresh} from "./authn-refresh" export function normalizeServices(services, data) { - return services.map(service => normalizeService(service, data)) + return services + .map(service => normalizeService(service, data)) + .filter(Boolean) } const serviceNormalizers = { diff --git a/packages/fcl/src/normalizers/service/user-signature.js b/packages/fcl/src/normalizers/service/user-signature.js index 0c25e5375..a0ab7aaba 100644 --- a/packages/fcl/src/normalizers/service/user-signature.js +++ b/packages/fcl/src/normalizers/service/user-signature.js @@ -12,11 +12,15 @@ export function normalizeUserSignature(service) { if (service == null) return null + if (!service["f_vsn"]) { + throw new Error("Invalid user-signature service") + } + switch (service["f_vsn"]) { case "1.0.0": return service default: - throw new Error("Invalid user-signature service") + return null } } diff --git a/packages/util-semver/.babelrc b/packages/util-semver/.babelrc new file mode 100644 index 000000000..67fc2886b --- /dev/null +++ b/packages/util-semver/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": [ + [ + "@babel/preset-env" + ] + ] +} diff --git a/packages/util-semver/package.json b/packages/util-semver/package.json new file mode 100644 index 000000000..47dfe67f7 --- /dev/null +++ b/packages/util-semver/package.json @@ -0,0 +1,32 @@ +{ + "name": "@onflow/util-semver", + "version": "1.0.0", + "description": "A lightweight semver implementation for use in FCL", + "main": "dist/index.js", + "module": "dist/index.module.js", + "source": "src/index.js", + "scripts": { + "prepublishOnly": "npm test && npm run build", + "test": "jest", + "build": "fcl-bundle", + "test:watch": "jest --watch", + "start": "fcl-bundle --watch" + }, + "devDependencies": { + "@onflow/fcl-bundle": "^1.3.1", + "jest": "^29.5.0" + }, + "dependencies": { + "@babel/runtime": "^7.18.6" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/onflow/fcl-js.git" + }, + "author": "Flow Blockchain", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/onflow/fcl-js/issues" + }, + "homepage": "https://onflow.org" +} diff --git a/packages/util-semver/src/compare-identifiers.js b/packages/util-semver/src/compare-identifiers.js new file mode 100644 index 000000000..e4276fa3b --- /dev/null +++ b/packages/util-semver/src/compare-identifiers.js @@ -0,0 +1,36 @@ +/* +compareIdentifiers was taken from semver package, which is licensed under ISC + +GitHub: https://github.com/npm/node-semver/ +NPM: https://www.npmjs.com/package/semver +License: https://github.com/npm/node-semver/blob/main/LICENSE + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +const numeric = /^[0-9]+$/ +export const compareIdentifiers = (a, b) => { + const anum = numeric.test(a) + const bnum = numeric.test(b) + + if (anum && bnum) { + a = +a + b = +b + } + + return a === b ? 0 : anum && !bnum ? -1 : bnum && !anum ? 1 : a < b ? -1 : 1 +} diff --git a/packages/util-semver/src/compare.js b/packages/util-semver/src/compare.js new file mode 100644 index 000000000..eb44f7992 --- /dev/null +++ b/packages/util-semver/src/compare.js @@ -0,0 +1,45 @@ +import {compareIdentifiers} from "./compare-identifiers" + +// Official Semver Regex https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +const VERSION_REGEX = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + +/** + * Compares two semver versions + * @param {string} a - The first version to compare + * @param {string} b - The second version to compare + * @returns {number} - Returns 1 if a is greater than b, -1 if a is less than b, and 0 if they are equal + */ +export const compare = (a, b) => { + if (typeof a !== "string" || typeof b !== "string") + throw new Error("Invalid input") + + const vsnA = a.match(VERSION_REGEX) + const vsnB = b.match(VERSION_REGEX) + + if (!vsnA || !vsnB) throw new Error("Invalid input") + + for (let i = 1; i <= 3; i++) { + // If either is greater than the other, return + if (parseInt(vsnA[i]) > parseInt(vsnB[i])) return 1 + if (parseInt(vsnA[i]) < parseInt(vsnB[i])) return -1 + } + + // If equal, compare prerelease + if (vsnA[4] && vsnB[4]) { + const prereleaseA = (vsnA[4] || "").split(".") + const prereleaseB = (vsnB[4] || "").split(".") + + for (let i = 0; i < Math.max(prereleaseA.length, prereleaseB.length); i++) { + // If one identifier has more fields than the other & the rest is equal, the one with more fields is greater + if (prereleaseA[i] === undefined) return -1 + if (prereleaseB[i] === undefined) return 1 + + // Compare field identifiers + const cmp = compareIdentifiers(prereleaseA[i], prereleaseB[i]) + if (cmp !== 0) return cmp + } + } + + return 0 +} diff --git a/packages/util-semver/src/compare.test.js b/packages/util-semver/src/compare.test.js new file mode 100644 index 000000000..62f6448ae --- /dev/null +++ b/packages/util-semver/src/compare.test.js @@ -0,0 +1,33 @@ +import {compare} from "./compare" + +// taken from semver.org +const sortOrder = [ + "1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0", + "1.0.0 < 2.0.0 < 2.1.0 < 2.1.1", + "1.0.0-alpha < 1.0.0", +] + +describe("semver compare", () => { + // Check that examples are sorted properly + sortOrder.forEach((order, i) => { + const items = order.split(" < ") + test(`sorts order #${i} correctly`, () => { + expect([...items].sort(compare)).toEqual(items) + }) + }) + + test(`compares equivalent versions properly`, () => { + expect(compare("1.0.0", "1.0.0")).toBe(0) + expect(compare("1.0.0+build", "1.0.0")).toBe(0) + expect(compare("1.0.0+build", "1.0.0+otherbuild")).toBe(0) + }) + + test(`throws on invalid input`, () => { + expect(() => compare("1.0.0", "")).toThrow() + expect(() => compare("", "1.0.0")).toThrow() + expect(() => compare("", "")).toThrow() + expect(() => compare("1.0.0", null)).toThrow() + expect(() => compare(null, "1.0.0")).toThrow() + expect(() => compare("1.0.a", "1.0.0")).toThrow() + }) +}) diff --git a/packages/util-semver/src/index.js b/packages/util-semver/src/index.js new file mode 100644 index 000000000..0088eb8e2 --- /dev/null +++ b/packages/util-semver/src/index.js @@ -0,0 +1 @@ +export {compare} from "./compare"