From 54e6b53cc2133eda7b11d28c3c2332dd6565f368 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 09:07:57 -0500 Subject: [PATCH 01/16] update semver helpers to handle 4 digit versions --- src/background/backgroundPlatform.ts | 4 +- src/contentScript/contentScriptPlatform.ts | 4 +- src/data/service/errorService.ts | 2 +- src/extensionPages/extensionPagePlatform.ts | 4 +- src/testUtils/testAfterEnv.ts | 3 +- src/types/helpers.test.ts | 131 ++++++++++++++++++++ src/types/helpers.ts | 27 +++- 7 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 src/types/helpers.test.ts diff --git a/src/background/backgroundPlatform.ts b/src/background/backgroundPlatform.ts index 2ec60d2f57..e97ffa6096 100644 --- a/src/background/backgroundPlatform.ts +++ b/src/background/backgroundPlatform.ts @@ -42,7 +42,9 @@ class BackgroundPlatform extends PlatformBase { constructor() { super( "background", - validateSemVerString(browser.runtime.getManifest().version), + validateSemVerString(browser.runtime.getManifest().version, { + coerce: true, + }), ); } diff --git a/src/contentScript/contentScriptPlatform.ts b/src/contentScript/contentScriptPlatform.ts index c15915ebbc..b1f9db6a0e 100644 --- a/src/contentScript/contentScriptPlatform.ts +++ b/src/contentScript/contentScriptPlatform.ts @@ -93,7 +93,9 @@ class ContentScriptPlatform extends PlatformBase { constructor() { super( "contentScript", - validateSemVerString(browser.runtime.getManifest().version), + validateSemVerString(browser.runtime.getManifest().version, { + coerce: true, + }), ); } diff --git a/src/data/service/errorService.ts b/src/data/service/errorService.ts index 7c0e464bd8..d4062d93b9 100644 --- a/src/data/service/errorService.ts +++ b/src/data/service/errorService.ts @@ -77,7 +77,7 @@ export async function selectExtraContext( ): Promise { const { version, manifest_version: manifestVersion } = browser.runtime.getManifest(); - const extensionVersion = validateSemVerString(version); + const extensionVersion = validateSemVerString(version, { coerce: true }); const extraContext: UnknownObject & { extensionVersion: SemVerString } = { extensionVersion, manifestVersion, diff --git a/src/extensionPages/extensionPagePlatform.ts b/src/extensionPages/extensionPagePlatform.ts index fc4b37844e..3a61860f0a 100644 --- a/src/extensionPages/extensionPagePlatform.ts +++ b/src/extensionPages/extensionPagePlatform.ts @@ -58,7 +58,9 @@ class ExtensionPagePlatform extends PlatformBase { constructor() { super( "extension", - validateSemVerString(browser.runtime.getManifest().version), + validateSemVerString(browser.runtime.getManifest().version, { + coerce: true, + }), ); } diff --git a/src/testUtils/testAfterEnv.ts b/src/testUtils/testAfterEnv.ts index da5f8c4b6c..99483e84d4 100644 --- a/src/testUtils/testAfterEnv.ts +++ b/src/testUtils/testAfterEnv.ts @@ -35,7 +35,8 @@ browser.runtime.onMessage.addListener = jest.fn(); browser.runtime.id = "mpjjildTESTIDkfjnepo"; browser.runtime.getManifest = jest.fn().mockReturnValue({ - version: "1.5.2", + // Validate that 4-digit version numbers are supported + version: "1.5.2.3000", }); browser.runtime.getURL = (path) => `chrome-extension://abcxyz/${path}`; diff --git a/src/types/helpers.test.ts b/src/types/helpers.test.ts new file mode 100644 index 0000000000..f9713cb17f --- /dev/null +++ b/src/types/helpers.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { testIsSemVerString, validateSemVerString } from "@/types/helpers"; + +describe("types/helpers.ts", () => { + describe("testIsSemVerString", () => { + it.each([ + { + value: "1.2.3", + allowLeadingV: false, + coerce: false, + expected: true, + }, + { + value: "1.2.3", + allowLeadingV: true, + coerce: true, + expected: true, + }, + { value: "v1.2.3", allowLeadingV: true, coerce: false, expected: true }, + { value: "v1.2.3", allowLeadingV: true, coerce: true, expected: true }, + { value: "v1.2.3", allowLeadingV: false, coerce: false, expected: false }, + { + value: "1.2.3.4", + allowLeadingV: false, + coerce: false, + expected: false, + }, + { + value: "1.2.3.4", + allowLeadingV: false, + coerce: true, + expected: true, + }, + { + value: "v1.2.3.4", + allowLeadingV: true, + coerce: true, + expected: true, + }, + ])( + "$value with allowLeadingV: $allowLeadingV and coerce: $coerce returns $expected", + ({ value, allowLeadingV, coerce, expected }) => { + expect(testIsSemVerString(value, { allowLeadingV, coerce })).toBe( + expected, + ); + }, + ); + }); + + describe("validateSemVerString", () => { + it.each([ + { + value: "1.2.3", + allowLeadingV: false, + coerce: false, + expected: "1.2.3", + }, + { + value: "v1.2.3", + allowLeadingV: true, + coerce: false, + expected: "v1.2.3", + }, + { + value: "1.2.3.4", + allowLeadingV: false, + coerce: true, + expected: "1.2.3", + }, + { + value: "v1.2.3.4", + allowLeadingV: true, + coerce: true, + expected: "v1.2.3", + }, + ])( + "$value with allowLeadingV: $allowLeadingV and coerce: $coerce returns $expected", + ({ value, allowLeadingV, coerce, expected }) => { + expect(validateSemVerString(value, { allowLeadingV, coerce })).toBe( + expected, + ); + }, + ); + + it.each([ + { + value: "v1.2.3", + allowLeadingV: false, + coerce: false, + }, + { + value: "1.2.3.4", + allowLeadingV: false, + coerce: false, + }, + { + value: "v1.2.3.4", + allowLeadingV: false, + coerce: true, + }, + { + value: "v1.2.3.4", + allowLeadingV: true, + coerce: false, + }, + ])( + "$value with allowLeadingV: $allowLeadingV and coerce: $coerce throws an error", + ({ value, allowLeadingV, coerce }) => { + expect(() => + validateSemVerString(value, { allowLeadingV, coerce }), + ).toThrow(); + }, + ); + }); +}); diff --git a/src/types/helpers.ts b/src/types/helpers.ts index a1776d8478..f36ca00cec 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { valid as semVerValid } from "semver"; +import { valid as semVerValid, coerce as semVerCoerce } from "semver"; import { startsWith } from "lodash"; import { @@ -154,7 +154,10 @@ export function validateTimestamp(value: string): Timestamp { export function validateSemVerString( value: string, // Default to `false` to be stricter. - { allowLeadingV = false }: { allowLeadingV?: boolean } = {}, + { + allowLeadingV = false, + coerce = false, + }: { allowLeadingV?: boolean; coerce?: boolean } = {}, ): SemVerString { if (value == null) { // We don't have strictNullChecks on, so null values will find there way here. We should pass them along. Eventually @@ -162,7 +165,16 @@ export function validateSemVerString( return value as SemVerString; } - if (testIsSemVerString(value, { allowLeadingV })) { + if (testIsSemVerString(value, { allowLeadingV, coerce })) { + if (coerce) { + const coerced = semVerValid(semVerCoerce(value)); + if (value.startsWith("v")) { + return `v${coerced}` as SemVerString; + } + + return coerced as SemVerString; + } + return value; } @@ -175,9 +187,14 @@ export function testIsSemVerString( value: string, // FIXME: the SemVerString type wasn't intended to support a leading `v`. See documentation // Default to `false` to be stricter. - { allowLeadingV = false }: { allowLeadingV?: boolean } = {}, + { + allowLeadingV = false, + coerce = false, + }: { allowLeadingV?: boolean; coerce?: boolean } = {}, ): value is SemVerString { - if (semVerValid(value) != null) { + const _value = coerce ? semVerCoerce(value) : value; + + if (semVerValid(_value) != null) { return allowLeadingV || !startsWith(value, "v"); } From f37fbf2d3af25adac7d8adf4796741d093f1e69f Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 09:11:46 -0500 Subject: [PATCH 02/16] fix deployment check --- src/utils/deploymentUtils.test.ts | 7 ++++--- src/utils/deploymentUtils.ts | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/utils/deploymentUtils.test.ts b/src/utils/deploymentUtils.test.ts index 2c97242627..18dd47206c 100644 --- a/src/utils/deploymentUtils.test.ts +++ b/src/utils/deploymentUtils.test.ts @@ -151,9 +151,10 @@ describe("checkExtensionUpdateRequired", () => { test("update not required", () => { const { deployment, modDefinition } = activatableDeploymentFactory(); - (modDefinition.metadata.extensionVersion as any) = `>=${ - browser.runtime.getManifest().version - }`; + (modDefinition.metadata.extensionVersion as any) = + `>=${validateSemVerString(browser.runtime.getManifest().version, { + coerce: true, + })}`; expect( checkExtensionUpdateRequired([{ deployment, modDefinition }]), diff --git a/src/utils/deploymentUtils.ts b/src/utils/deploymentUtils.ts index 27300ce05e..63b77e7efb 100644 --- a/src/utils/deploymentUtils.ts +++ b/src/utils/deploymentUtils.ts @@ -25,7 +25,7 @@ import { type IntegrationDependency, type SanitizedIntegrationConfig, } from "@/integrations/integrationTypes"; -import { validateUUID } from "@/types/helpers"; +import { validateSemVerString, validateUUID } from "@/types/helpers"; import { type Except } from "type-fest"; import { PIXIEBRIX_INTEGRATION_ID } from "@/integrations/constants"; import getUnconfiguredComponentIntegrations from "@/integrations/util/getUnconfiguredComponentIntegrations"; @@ -124,7 +124,8 @@ export function checkExtensionUpdateRequired( activatableDeployments: ActivatableDeployment[] = [], ): boolean { // Check that the user's extension can run the deployment - const { version: extensionVersion } = browser.runtime.getManifest(); + const { version } = browser.runtime.getManifest(); + const extensionVersion = validateSemVerString(version, { coerce: true }); const versionRanges = compact( activatableDeployments.map( ({ modDefinition }) => modDefinition.metadata.extensionVersion, From 04e99ee61affe9b66b6fc8cf071400edebbc9971 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 09:16:58 -0500 Subject: [PATCH 03/16] make 4th digit larger to reflect actual usage --- src/types/helpers.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/types/helpers.test.ts b/src/types/helpers.test.ts index f9713cb17f..04dee28fe1 100644 --- a/src/types/helpers.test.ts +++ b/src/types/helpers.test.ts @@ -36,19 +36,19 @@ describe("types/helpers.ts", () => { { value: "v1.2.3", allowLeadingV: true, coerce: true, expected: true }, { value: "v1.2.3", allowLeadingV: false, coerce: false, expected: false }, { - value: "1.2.3.4", + value: "1.2.3.4000", allowLeadingV: false, coerce: false, expected: false, }, { - value: "1.2.3.4", + value: "1.2.3.4000", allowLeadingV: false, coerce: true, expected: true, }, { - value: "v1.2.3.4", + value: "v1.2.3.4000", allowLeadingV: true, coerce: true, expected: true, @@ -78,13 +78,13 @@ describe("types/helpers.ts", () => { expected: "v1.2.3", }, { - value: "1.2.3.4", + value: "1.2.3.4000", allowLeadingV: false, coerce: true, expected: "1.2.3", }, { - value: "v1.2.3.4", + value: "v1.2.3.4000", allowLeadingV: true, coerce: true, expected: "v1.2.3", @@ -105,17 +105,17 @@ describe("types/helpers.ts", () => { coerce: false, }, { - value: "1.2.3.4", + value: "1.2.3.4000", allowLeadingV: false, coerce: false, }, { - value: "v1.2.3.4", + value: "v1.2.3.4000", allowLeadingV: false, coerce: true, }, { - value: "v1.2.3.4", + value: "v1.2.3.4000", allowLeadingV: true, coerce: false, }, From 6f0d854ed9dc2d62d1e365372a220e15b202c134 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 09:38:25 -0500 Subject: [PATCH 04/16] replace direct access with getExtensionVersion() --- package-lock.json | 4 ++-- package.json | 2 +- src/background/backgroundPlatform.ts | 9 ++------- src/background/deploymentUpdater.ts | 2 +- src/background/installer.ts | 9 ++++++--- src/background/messenger/external/api.ts | 1 + src/background/telemetry.ts | 13 ++++++------- src/contentScript/contentScriptPlatform.ts | 9 ++------- src/data/service/errorService.ts | 8 ++++---- src/extensionConsole/pages/UpdateBanner.tsx | 3 ++- .../pages/settings/useDiagnostics.ts | 8 +++++--- src/extensionPages/extensionPagePlatform.ts | 9 ++------- src/utils/deploymentUtils.test.ts | 5 ++--- src/utils/deploymentUtils.ts | 7 ++++--- src/utils/extensionUtils.ts | 9 +++++++-- 15 files changed, 47 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62b69ce204..21b1f0391e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pixiebrix/extension", - "version": "1.8.13-alpha.1", + "version": "1.8.13.3000", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pixiebrix/extension", - "version": "1.8.13-alpha.1", + "version": "1.8.13.3000", "license": "AGPL-3.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/package.json b/package.json index 6cd208bc9b..ad1f2602bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pixiebrix/extension", - "version": "1.8.13-alpha.1", + "version": "1.8.13.3000", "description": "PixieBrix Browser Extension", "scripts": { "test": "TZ=UTC jest", diff --git a/src/background/backgroundPlatform.ts b/src/background/backgroundPlatform.ts index e97ffa6096..f828fdf19a 100644 --- a/src/background/backgroundPlatform.ts +++ b/src/background/backgroundPlatform.ts @@ -23,8 +23,8 @@ import type { NetworkRequestConfig } from "@/types/networkTypes"; import type { RemoteResponse } from "@/types/contract"; import { performConfiguredRequest } from "@/background/requests"; import BackgroundLogger from "@/telemetry/BackgroundLogger"; -import { validateSemVerString } from "@/types/helpers"; import { PlatformBase } from "@/platform/platformBase"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * Background platform implementation. Currently, just makes API requests. @@ -40,12 +40,7 @@ class BackgroundPlatform extends PlatformBase { }); constructor() { - super( - "background", - validateSemVerString(browser.runtime.getManifest().version, { - coerce: true, - }), - ); + super("background", getExtensionVersion()); } override get logger(): PlatformProtocol["logger"] { diff --git a/src/background/deploymentUpdater.ts b/src/background/deploymentUpdater.ts index 6c953e76e2..df0d574922 100644 --- a/src/background/deploymentUpdater.ts +++ b/src/background/deploymentUpdater.ts @@ -608,7 +608,7 @@ async function activateDeploymentsInBackground({ ); // Version to report to the server. - const { version: extensionVersionString } = browser.runtime.getManifest(); + const extensionVersionString = getExtensionVersion(); const extensionVersion = parseSemVer(extensionVersionString); const deploymentsByActivationMethod = await Promise.all( diff --git a/src/background/installer.ts b/src/background/installer.ts index a5829b561b..14123f958a 100644 --- a/src/background/installer.ts +++ b/src/background/installer.ts @@ -36,7 +36,10 @@ import { import { Events } from "@/telemetry/events"; import { DEFAULT_SERVICE_URL, UNINSTALL_URL } from "@/urlConstants"; import { CONTROL_ROOM_TOKEN_INTEGRATION_ID } from "@/integrations/constants"; -import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; +import { + getExtensionConsoleUrl, + getExtensionVersion, +} from "@/utils/extensionUtils"; import { oncePerSession } from "@/mv3/SessionStorage"; import { resetFeatureFlagsCache } from "@/auth/featureFlagStorage"; @@ -223,7 +226,7 @@ export async function showInstallPage({ // https://developer.chrome.com/docs/extensions/reference/runtime/#event-onInstalled // https://developer.chrome.com/docs/extensions/reference/runtime/#type-OnInstalledReason console.debug("onInstalled", { reason, previousVersion }); - const { version } = browser.runtime.getManifest(); + const version = getExtensionVersion(); if (reason === "install") { void recordEvent({ @@ -309,7 +312,7 @@ export function getAvailableVersion(): typeof _availableVersion { */ export function isUpdateAvailable(): boolean { const available = getAvailableVersion(); - const installed = browser.runtime.getManifest().version; + const installed = getExtensionVersion(); return ( Boolean(available) && installed !== available && gt(available, installed) ); diff --git a/src/background/messenger/external/api.ts b/src/background/messenger/external/api.ts index 83801cd14f..65344ad868 100644 --- a/src/background/messenger/external/api.ts +++ b/src/background/messenger/external/api.ts @@ -25,6 +25,7 @@ import * as local from "@/background/messenger/external/_implementation"; import { readPartnerAuthData } from "@/auth/authStorage"; export const connectPage = liftExternal("CONNECT_PAGE", async () => + // TODO: Validate that this works with the 4-digit version number browser.runtime.getManifest(), ); diff --git a/src/background/telemetry.ts b/src/background/telemetry.ts index a7e1c8db18..d3eda3a2e2 100644 --- a/src/background/telemetry.ts +++ b/src/background/telemetry.ts @@ -29,7 +29,7 @@ import { count as registrySize } from "@/registry/packageRegistry"; import { count as logSize } from "@/telemetry/logging"; import { count as traceSize } from "@/telemetry/trace"; import { getUUID } from "@/telemetry/telemetryHelpers"; -import { getTabsWithAccess } from "@/utils/extensionUtils"; +import { getExtensionVersion, getTabsWithAccess } from "@/utils/extensionUtils"; import { type Event } from "@/telemetry/events"; const EVENT_BUFFER_DEBOUNCE_MS = 2000; @@ -274,7 +274,8 @@ export async function TEST_flushAll(): Promise { async function collectUserSummary(): Promise { const { os } = await browser.runtime.getPlatformInfo(); - const { version, version_name: versionName } = browser.runtime.getManifest(); + const { version_name: versionName } = browser.runtime.getManifest(); + const version = getExtensionVersion(); // Not supported on Chromium, and may require additional permissions // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/getBrowserInfo // const {name: browserName} = await browser.runtime.getBrowserInfo(); @@ -336,11 +337,9 @@ export async function recordEvent({ data: UnknownObject | undefined; }): Promise { if (await allowsTrack()) { - const { - version, - version_name: versionName, - manifest_version: manifestVersion, - } = browser.runtime.getManifest(); + const { version_name: versionName, manifest_version: manifestVersion } = + browser.runtime.getManifest(); + const version = getExtensionVersion(); const telemetryEvent = { uid: await getUUID(), event, diff --git a/src/contentScript/contentScriptPlatform.ts b/src/contentScript/contentScriptPlatform.ts index b1f9db6a0e..908f33fd42 100644 --- a/src/contentScript/contentScriptPlatform.ts +++ b/src/contentScript/contentScriptPlatform.ts @@ -49,7 +49,6 @@ import { writeToClipboard } from "@/utils/clipboardUtils"; import { snippetRegistry } from "@/contentScript/snippetShortcutMenu/snippetShortcutMenuController"; import BackgroundLogger from "@/telemetry/BackgroundLogger"; import * as sidebarController from "@/contentScript/sidebarController"; -import { validateSemVerString } from "@/types/helpers"; import type { UUID } from "@/types/stringTypes"; import { PlatformBase } from "@/platform/platformBase"; import type { Nullishable } from "@/utils/nullishUtils"; @@ -61,6 +60,7 @@ import { InteractiveLoginRequiredError } from "@/errors/authErrors"; import { deferLogin } from "@/contentScript/integrations/deferredLoginController"; import { flagOn } from "@/auth/featureFlagStorage"; import { selectionMenuActionRegistry } from "@/contentScript/textSelectionMenu/selectionMenuController"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * @file Platform definition for mods running in a content script @@ -91,12 +91,7 @@ class ContentScriptPlatform extends PlatformBase { }); constructor() { - super( - "contentScript", - validateSemVerString(browser.runtime.getManifest().version, { - coerce: true, - }), - ); + super("contentScript", getExtensionVersion()); } override capabilities: PlatformCapability[] = [ diff --git a/src/data/service/errorService.ts b/src/data/service/errorService.ts index d4062d93b9..5b43e09930 100644 --- a/src/data/service/errorService.ts +++ b/src/data/service/errorService.ts @@ -22,7 +22,7 @@ import { selectSpecificError, } from "@/errors/errorHelpers"; import { allowsTrack } from "@/telemetry/dnt"; -import { uuidv4, validateSemVerString } from "@/types/helpers"; +import { uuidv4 } from "@/types/helpers"; import { getUserData } from "@/auth/authStorage"; import { isAppRequestError, @@ -42,6 +42,7 @@ import { isObject } from "@/utils/objectUtils"; import type { Timestamp } from "@/types/stringTypes"; import { flagOn } from "@/auth/featureFlagStorage"; import { selectAbsoluteUrl } from "@/utils/urlUtils"; +import { getExtensionVersion } from "@/utils/extensionUtils"; const EVENT_BUFFER_DEBOUNCE_MS = 2000; const EVENT_BUFFER_MAX_MS = 10_000; @@ -75,9 +76,8 @@ async function flush(): Promise { export async function selectExtraContext( error: Error | SerializedError, ): Promise { - const { version, manifest_version: manifestVersion } = - browser.runtime.getManifest(); - const extensionVersion = validateSemVerString(version, { coerce: true }); + const { manifest_version: manifestVersion } = browser.runtime.getManifest(); + const extensionVersion = getExtensionVersion(); const extraContext: UnknownObject & { extensionVersion: SemVerString } = { extensionVersion, manifestVersion, diff --git a/src/extensionConsole/pages/UpdateBanner.tsx b/src/extensionConsole/pages/UpdateBanner.tsx index 23b9d01492..4c189c3af0 100644 --- a/src/extensionConsole/pages/UpdateBanner.tsx +++ b/src/extensionConsole/pages/UpdateBanner.tsx @@ -22,13 +22,14 @@ import reportError from "@/telemetry/reportError"; import Banner from "@/components/banner/Banner"; import { gt } from "semver"; import useAsyncState from "@/hooks/useAsyncState"; +import { getExtensionVersion } from "@/utils/extensionUtils"; // XXX: move this kind of async state to the Redux state. export function useUpdateAvailable(): boolean { const { data: updateAvailable } = useAsyncState(async () => { try { const available = await getAvailableVersion(); - const installed = browser.runtime.getManifest().version; + const installed = getExtensionVersion(); return available && installed !== available && gt(available, installed); } catch (error) { reportError(error); diff --git a/src/extensionConsole/pages/settings/useDiagnostics.ts b/src/extensionConsole/pages/settings/useDiagnostics.ts index 378e820d3b..2f9fee6529 100644 --- a/src/extensionConsole/pages/settings/useDiagnostics.ts +++ b/src/extensionConsole/pages/settings/useDiagnostics.ts @@ -21,7 +21,7 @@ import useExtensionPermissions, { type DetailedPermissions, } from "@/permissions/useExtensionPermissions"; import { type UnresolvedModComponent } from "@/types/modComponentTypes"; -import { compact, pick, uniqBy } from "lodash"; +import { compact, uniqBy } from "lodash"; import { type StorageEstimate } from "@/types/browserTypes"; import { count as registrySize } from "@/registry/packageRegistry"; import { count as logSize } from "@/telemetry/logging"; @@ -30,6 +30,7 @@ import { count as eventsSize } from "@/background/telemetry"; import useUserAction from "@/hooks/useUserAction"; import download from "downloadjs"; import filenamify from "filenamify"; +import { getExtensionVersion } from "@/utils/extensionUtils"; async function collectDiagnostics({ extensions, @@ -38,10 +39,11 @@ async function collectDiagnostics({ extensions: UnresolvedModComponent[]; permissions: DetailedPermissions; }) { - const manifest = browser.runtime.getManifest(); + const { version_name } = browser.runtime.getManifest(); + const version = getExtensionVersion(); return { userAgent: window.navigator.userAgent, - manifest: pick(manifest, ["version", "version_name"]), + manifest: { version, version_name }, permissions, storage: { storageEstimate: (await navigator.storage.estimate()) as StorageEstimate, diff --git a/src/extensionPages/extensionPagePlatform.ts b/src/extensionPages/extensionPagePlatform.ts index 3a61860f0a..0ba10c8f8e 100644 --- a/src/extensionPages/extensionPagePlatform.ts +++ b/src/extensionPages/extensionPagePlatform.ts @@ -19,7 +19,6 @@ import { type PlatformProtocol } from "@/platform/platformProtocol"; import { hideNotification, showNotification } from "@/utils/notify"; import type { PlatformCapability } from "@/platform/capabilities"; import BackgroundLogger from "@/telemetry/BackgroundLogger"; -import { validateSemVerString } from "@/types/helpers"; import type { UUID } from "@/types/stringTypes"; import { traces, @@ -33,6 +32,7 @@ import type { NetworkRequestConfig } from "@/types/networkTypes"; import type { RemoteResponse } from "@/types/contract"; import integrationRegistry from "@/integrations/registry"; import { performConfiguredRequest } from "@/background/requests"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * The extension page platform. @@ -56,12 +56,7 @@ class ExtensionPagePlatform extends PlatformBase { }); constructor() { - super( - "extension", - validateSemVerString(browser.runtime.getManifest().version, { - coerce: true, - }), - ); + super("extension", getExtensionVersion()); } override alert = window.alert; diff --git a/src/utils/deploymentUtils.test.ts b/src/utils/deploymentUtils.test.ts index 18dd47206c..ba4f492edb 100644 --- a/src/utils/deploymentUtils.test.ts +++ b/src/utils/deploymentUtils.test.ts @@ -42,6 +42,7 @@ import { PIXIEBRIX_INTEGRATION_ID, } from "@/integrations/constants"; import getModDefinitionIntegrationIds from "@/integrations/util/getModDefinitionIntegrationIds"; +import { getExtensionVersion } from "@/utils/extensionUtils"; describe("makeUpdatedFilter", () => { test.each([[{ restricted: true }, { restricted: false }]])( @@ -152,9 +153,7 @@ describe("checkExtensionUpdateRequired", () => { test("update not required", () => { const { deployment, modDefinition } = activatableDeploymentFactory(); (modDefinition.metadata.extensionVersion as any) = - `>=${validateSemVerString(browser.runtime.getManifest().version, { - coerce: true, - })}`; + `>=${getExtensionVersion()}`; expect( checkExtensionUpdateRequired([{ deployment, modDefinition }]), diff --git a/src/utils/deploymentUtils.ts b/src/utils/deploymentUtils.ts index 63b77e7efb..cd32cc310b 100644 --- a/src/utils/deploymentUtils.ts +++ b/src/utils/deploymentUtils.ts @@ -25,11 +25,12 @@ import { type IntegrationDependency, type SanitizedIntegrationConfig, } from "@/integrations/integrationTypes"; -import { validateSemVerString, validateUUID } from "@/types/helpers"; +import { validateUUID } from "@/types/helpers"; import { type Except } from "type-fest"; import { PIXIEBRIX_INTEGRATION_ID } from "@/integrations/constants"; import getUnconfiguredComponentIntegrations from "@/integrations/util/getUnconfiguredComponentIntegrations"; import type { ActivatableDeployment } from "@/types/deploymentTypes"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * Returns `true` if a managed deployment is active (i.e., has not been remotely paused by an admin) @@ -124,8 +125,8 @@ export function checkExtensionUpdateRequired( activatableDeployments: ActivatableDeployment[] = [], ): boolean { // Check that the user's extension can run the deployment - const { version } = browser.runtime.getManifest(); - const extensionVersion = validateSemVerString(version, { coerce: true }); + + const extensionVersion = getExtensionVersion(); const versionRanges = compact( activatableDeployments.map( ({ modDefinition }) => modDefinition.metadata.extensionVersion, diff --git a/src/utils/extensionUtils.ts b/src/utils/extensionUtils.ts index 41c0d317cb..b4e84bf263 100644 --- a/src/utils/extensionUtils.ts +++ b/src/utils/extensionUtils.ts @@ -20,6 +20,8 @@ import { foreverPendingPromise } from "@/utils/promiseUtils"; import { type Promisable } from "type-fest"; import { isScriptableUrl } from "webext-content-scripts"; import { type Runtime } from "webextension-polyfill"; +import { validateSemVerString } from "@/types/helpers"; +import { type SemVerString } from "@/types/registryTypes"; export const SHORTCUTS_URL = "chrome://extensions/shortcuts"; type Command = "toggle-quick-bar"; @@ -65,8 +67,11 @@ export function getExtensionConsoleUrl(page?: string): string { return url.href; } -export function getExtensionVersion(): string { - return browser.runtime.getManifest().version; +// TODO: Add linting rule to prefer getExtensionVersion over browser.runtime.getManifest().version +export function getExtensionVersion(): SemVerString { + return validateSemVerString(browser.runtime.getManifest().version, { + coerce: true, + }); } /** If no update is available and downloaded yet, it will return a string explaining why */ From 21c4d14351bf25b7b42735865c0b630418e7f350 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 09:43:59 -0500 Subject: [PATCH 05/16] fix broken tests --- src/background/deploymentUpdater.test.ts | 2 +- src/background/restrictUnauthenticatedUrlAccess.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/background/deploymentUpdater.test.ts b/src/background/deploymentUpdater.test.ts index 1d5fcff8a4..4ea447cfa3 100644 --- a/src/background/deploymentUpdater.test.ts +++ b/src/background/deploymentUpdater.test.ts @@ -68,8 +68,8 @@ jest.mock("@/store/settings/settingsStorage"); jest.mock("@/hooks/useRefreshRegistries"); jest.mock("@/utils/extensionUtils", () => ({ + ...jest.requireActual("@/utils/extensionUtils"), forEachTab: jest.fn(), - getExtensionVersion: () => browser.runtime.getManifest().version, })); // Override manual mock to support `expect` assertions diff --git a/src/background/restrictUnauthenticatedUrlAccess.test.ts b/src/background/restrictUnauthenticatedUrlAccess.test.ts index aca607ff0b..d8d317d39e 100644 --- a/src/background/restrictUnauthenticatedUrlAccess.test.ts +++ b/src/background/restrictUnauthenticatedUrlAccess.test.ts @@ -36,6 +36,7 @@ jest.mock("@/auth/authStorage", () => ({ })); jest.mock("@/utils/extensionUtils", () => ({ + ...jest.requireActual("@/utils/extensionUtils"), forEachTab: jest.fn(), })); From 5313d68484bbed00a4ba9f14ceea2ea69566aeaf Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 09:55:11 -0500 Subject: [PATCH 06/16] fix manifest for connectPage --- src/background/messenger/external/api.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/background/messenger/external/api.ts b/src/background/messenger/external/api.ts index 65344ad868..6b519a90c8 100644 --- a/src/background/messenger/external/api.ts +++ b/src/background/messenger/external/api.ts @@ -23,10 +23,14 @@ import { _liftBackground as liftExternal } from "@/background/externalProtocol"; import * as local from "@/background/messenger/external/_implementation"; import { readPartnerAuthData } from "@/auth/authStorage"; +import { getExtensionVersion } from "@/utils/extensionUtils"; export const connectPage = liftExternal("CONNECT_PAGE", async () => - // TODO: Validate that this works with the 4-digit version number - browser.runtime.getManifest(), + // Ensure the version we send to the app is a valid semver. + ({ + ...browser.runtime.getManifest(), + version: getExtensionVersion(), + }), ); export const setExtensionAuth = liftExternal( From abc3c043122003eeed6575b9495416c23178d3d4 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 10:40:00 -0500 Subject: [PATCH 07/16] add 4th digit for all builds --- scripts/manifest.mjs | 24 +++++++++++------------- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index c8c641e550..462b4bc68c 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -19,10 +19,11 @@ import Policy from "csp-parse"; import { normalizeManifestPermissions } from "webext-permissions"; import { excludeDuplicatePatterns } from "webext-patterns"; -function getVersion(env, isBetaListing) { +function getVersion(env) { const stageMap = { alpha: 1000, beta: 2000, + release: 3000, }; // `manifest.json` only supports numbers in the version, so use the semver @@ -32,19 +33,16 @@ function getVersion(env, isBetaListing) { ); const { version, stage, stageNumber } = match.groups; - // Add 4th digit for alpha/beta release builds. Used to update the extension BETA listing in the Chrome Web Store. - if (isBetaListing) { - if (stage && stageNumber) { - // Ex: 1.8.13-alpha.1 -> 1.8.13.1001 - // Ex: 1.8.13-beta.55 -> 1.8.13.2055 - return `${version}.${stageMap[stage] + Number(stageNumber)}`; - } - - // Ex: 1.8.13.3000 -- Ensures that the release build version number is greater than the alpha/beta build version numbers - return `${version}.3000`; + // Add 4th digit for differentiating alpha/beta/stable release builds. + // Used primarily to update the extension BETA listing in the Chrome Web Store. + if (stage && stageNumber) { + // Ex: 1.8.13-alpha.1 -> 1.8.13.1001 + // Ex: 1.8.13-beta.55 -> 1.8.13.2055 + return `${version}.${stageMap[stage] + Number(stageNumber)}`; } - return version; + // Ex: 1.8.13.3000 -- Ensures that the release build version number is greater than the alpha/beta build version numbers + return `${version}.${stageMap.release}`; } function getVersionName(env, isProduction) { @@ -146,7 +144,7 @@ function addInternalUrlsToContentScripts(manifest, internal) { function customizeManifest(manifestV2, options = {}) { const { isProduction, manifestVersion, env = {}, isBeta } = options; const manifest = structuredClone(manifestV2); - manifest.version = getVersion(env, isBeta); + manifest.version = getVersion(env); manifest.version_name = getVersionName(env, isProduction); if (!isProduction) { diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index d112349d5c..6ac3d9a6be 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./types/helpers.test.ts", "../end-to-end-tests/auth.setup.ts", "../end-to-end-tests/env.ts", "../end-to-end-tests/fixtures/extensionBase.ts", From feb172333b1aa6f7d021247d30e42902ba01cc38 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 10:55:20 -0500 Subject: [PATCH 08/16] manifest tests --- scripts/__snapshots__/manifest.test.js.snap | 12 ++- scripts/manifest.test.js | 87 ++++++++++++++------- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/scripts/__snapshots__/manifest.test.js.snap b/scripts/__snapshots__/manifest.test.js.snap index 98bae6d7bd..8b1e8ea04e 100644 --- a/scripts/__snapshots__/manifest.test.js.snap +++ b/scripts/__snapshots__/manifest.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`customizeManifest beta 1`] = ` +exports[`customizeManifest release builds beta 1`] = ` { "action": { "default_icon": { @@ -124,6 +124,8 @@ exports[`customizeManifest beta 1`] = ` "storage": { "managed_schema": "managedStorageSchema.json", }, + "version": "1.8.13.3000", + "version_name": "1.8.13", "web_accessible_resources": [ { "matches": [ @@ -149,7 +151,7 @@ exports[`customizeManifest beta 1`] = ` } `; -exports[`customizeManifest mv2 1`] = ` +exports[`customizeManifest release builds mv2 1`] = ` { "author": "PixieBrix, Inc.", "background": { @@ -264,6 +266,8 @@ exports[`customizeManifest mv2 1`] = ` "storage": { "managed_schema": "managedStorageSchema.json", }, + "version": "1.8.13.3000", + "version_name": "1.8.13", "web_accessible_resources": [ "css/*", "bundles/*", @@ -282,7 +286,7 @@ exports[`customizeManifest mv2 1`] = ` } `; -exports[`customizeManifest mv3 1`] = ` +exports[`customizeManifest release builds mv3 1`] = ` { "action": { "default_icon": { @@ -406,6 +410,8 @@ exports[`customizeManifest mv3 1`] = ` "storage": { "managed_schema": "managedStorageSchema.json", }, + "version": "1.8.13.3000", + "version_name": "1.8.13", "web_accessible_resources": [ { "matches": [ diff --git a/scripts/manifest.test.js b/scripts/manifest.test.js index 3d71e281e1..aa1c12a7e1 100644 --- a/scripts/manifest.test.js +++ b/scripts/manifest.test.js @@ -15,9 +15,6 @@ * along with this program. If not, see . */ -/* eslint-disable @shopify/jest/no-snapshots -- We want to specifically commit the entire customized manifest as a snapshot */ -/* eslint-disable no-restricted-imports -- Aliases don't work outside built files */ - import { omit } from "lodash"; import manifest from "../src/manifest.json"; import { loadEnv } from "./env.mjs"; @@ -25,35 +22,65 @@ import customizeManifest from "./manifest.mjs"; loadEnv(); -const cleanCustomize = (...args) => - omit(customizeManifest(...args), ["version", "version_name", "key"]); +const cleanCustomize = (...args) => omit(customizeManifest(...args), ["key"]); describe("customizeManifest", () => { - test("mv2", () => { - expect( - cleanCustomize(manifest, { - env: process.env, - isProduction: true, - }), - ).toMatchSnapshot(); - }); - test("mv3", () => { - expect( - cleanCustomize(manifest, { - env: process.env, - isProduction: true, - manifestVersion: 3, - }), - ).toMatchSnapshot(); + describe("release builds", () => { + test("mv2", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + }), + ).toMatchSnapshot(); + }); + + test("mv3", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + manifestVersion: 3, + }), + ).toMatchSnapshot(); + }); + + test("beta", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + manifestVersion: 3, + isBeta: true, + }), + ).toMatchSnapshot(); + }); }); - test("beta", () => { - expect( - cleanCustomize(manifest, { - env: process.env, - isProduction: true, - manifestVersion: 3, - isBeta: true, - }), - ).toMatchSnapshot(); + + describe("alpha/beta versioning", () => { + test("alpha version", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13-alpha.123" }, + isProduction: true, + manifestVersion: 3, + }), + ).toContainEntry(["version", "1.8.13.1123"]); + }); + + test("beta version", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13-beta.123" }, + isProduction: true, + manifestVersion: 3, + }), + ).toContainEntry(["version", "1.8.13.2123"]); + }); }); }); From 09a691b27f53bc3a8cde34a0cbe5d1a07281b46a Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 10:56:22 -0500 Subject: [PATCH 09/16] add release version test --- scripts/manifest.test.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/manifest.test.js b/scripts/manifest.test.js index aa1c12a7e1..b61212e83c 100644 --- a/scripts/manifest.test.js +++ b/scripts/manifest.test.js @@ -60,7 +60,7 @@ describe("customizeManifest", () => { }); }); - describe("alpha/beta versioning", () => { + describe("four digit versioning", () => { test("alpha version", () => { expect( cleanCustomize(manifest, { @@ -82,5 +82,16 @@ describe("customizeManifest", () => { }), ).toContainEntry(["version", "1.8.13.2123"]); }); + + test("release version", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + manifestVersion: 3, + }), + ).toContainEntry(["version", "1.8.13.3000"]); + }); }); }); From 471b5881d113ca4a32fbd94b1f01893bee468754 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 10:58:21 -0500 Subject: [PATCH 10/16] revert version change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad1f2602bb..6b444f57dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pixiebrix/extension", - "version": "1.8.13.3000", + "version": "1.8.13.alpha.1", "description": "PixieBrix Browser Extension", "scripts": { "test": "TZ=UTC jest", From 5fc5c8cc9fb720b262ef1fd954d868f6387dd2c0 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 11:08:17 -0500 Subject: [PATCH 11/16] fix package-lock version --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21b1f0391e..62b69ce204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pixiebrix/extension", - "version": "1.8.13.3000", + "version": "1.8.13-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pixiebrix/extension", - "version": "1.8.13.3000", + "version": "1.8.13-alpha.1", "license": "AGPL-3.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", From 93526f56b61f6df2a460554dacc442013224ae15 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 11:08:57 -0500 Subject: [PATCH 12/16] properly fix package.json version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b444f57dd..6cd208bc9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pixiebrix/extension", - "version": "1.8.13.alpha.1", + "version": "1.8.13-alpha.1", "description": "PixieBrix Browser Extension", "scripts": { "test": "TZ=UTC jest", From 92649996d1213ca19ba92c1ac6ae24b87610fda4 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 11:50:41 -0500 Subject: [PATCH 13/16] rename validateSemVerString -> normalizeSemVerString --- src/background/deploymentUpdater.test.ts | 6 +++--- .../convertToRecipeModal/ConvertToRecipeModalBody.tsx | 4 ++-- src/pageEditor/panes/save/saveHelpers.test.ts | 8 ++++---- src/pageEditor/sidebar/ModListItem.test.tsx | 4 ++-- src/pageEditor/sidebar/modals/CreateModModal.tsx | 6 +++--- src/pageEditor/starterBricks/base.ts | 4 ++-- src/platform/platformContext.ts | 4 ++-- src/registry/packageRegistry.test.ts | 4 ++-- src/runtime/pipelineTests/component.test.ts | 4 ++-- src/testUtils/factories/brickFactories.ts | 4 ++-- src/testUtils/factories/deploymentFactories.ts | 4 ++-- src/testUtils/factories/metadataFactory.ts | 4 ++-- src/testUtils/platformMock.ts | 4 ++-- src/types/helpers.test.ts | 8 ++++---- src/types/helpers.ts | 2 +- src/utils/deploymentUtils.test.ts | 4 ++-- src/utils/extensionUtils.ts | 4 ++-- src/utils/objToYaml.test.ts | 6 +++--- 18 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/background/deploymentUpdater.test.ts b/src/background/deploymentUpdater.test.ts index 4ea447cfa3..8252856bac 100644 --- a/src/background/deploymentUpdater.test.ts +++ b/src/background/deploymentUpdater.test.ts @@ -19,7 +19,7 @@ import { getModComponentState, saveModComponentState, } from "@/store/extensionsStorage"; -import { uuidv4, validateSemVerString } from "@/types/helpers"; +import { uuidv4, normalizeSemVerString } from "@/types/helpers"; import { appApiMock } from "@/testUtils/appApiMock"; import { omit } from "lodash"; import { syncDeployments } from "@/background/deploymentUpdater"; @@ -380,7 +380,7 @@ describe("syncDeployments", () => { _recipe: { id: deployment.package.package_id, name: deployment.package.name, - version: validateSemVerString("0.0.1"), + version: normalizeSemVerString("0.0.1"), updated_at: deployment.updated_at, sharing: sharingDefinitionFactory(), }, @@ -438,7 +438,7 @@ describe("syncDeployments", () => { _recipe: { id: deployment.package.package_id, name: deployment.package.name, - version: validateSemVerString("0.0.1"), + version: normalizeSemVerString("0.0.1"), updated_at: deployment.updated_at, sharing: sharingDefinitionFactory(), }, diff --git a/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx b/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx index dee4d8fffc..395e770853 100644 --- a/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx +++ b/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx @@ -26,7 +26,7 @@ import * as Yup from "yup"; import { PACKAGE_REGEX, testIsSemVerString, - validateSemVerString, + normalizeSemVerString, } from "@/types/helpers"; import { pick } from "lodash"; import Form from "@/components/form/Form"; @@ -136,7 +136,7 @@ const ConvertToRecipeModalBody: React.FunctionComponent = () => { () => ({ blueprintId: generatePackageId(scope, extension.label), name: extension.label, - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Created with the PixieBrix Page Editor", }), // eslint-disable-next-line react-hooks/exhaustive-deps -- initial values for the form, we calculate them once diff --git a/src/pageEditor/panes/save/saveHelpers.test.ts b/src/pageEditor/panes/save/saveHelpers.test.ts index 9653d4d777..059ccec207 100644 --- a/src/pageEditor/panes/save/saveHelpers.test.ts +++ b/src/pageEditor/panes/save/saveHelpers.test.ts @@ -22,7 +22,7 @@ import { replaceModComponent, selectExtensionPointIntegrations, } from "@/pageEditor/panes/save/saveHelpers"; -import { validateRegistryId, validateSemVerString } from "@/types/helpers"; +import { validateRegistryId, normalizeSemVerString } from "@/types/helpers"; import menuItemExtensionAdapter from "@/pageEditor/starterBricks/menuItem"; import { internalStarterBrickMetaFactory, @@ -190,7 +190,7 @@ describe("replaceModComponent round trip", () => { metadata: { id: makeInternalId(modDefinition.definitions.extensionPoint), name: "Internal Starter Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }, } as any); @@ -240,7 +240,7 @@ describe("replaceModComponent round trip", () => { metadata: { id: makeInternalId(modDefinition.definitions.extensionPoint), name: "Internal Starter Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }, } as any); @@ -300,7 +300,7 @@ describe("replaceModComponent round trip", () => { metadata: { id: makeInternalId(modDefinition.definitions.extensionPoint), name: "Internal Starter Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }, } as any); diff --git a/src/pageEditor/sidebar/ModListItem.test.tsx b/src/pageEditor/sidebar/ModListItem.test.tsx index e65369c906..c73ad116da 100644 --- a/src/pageEditor/sidebar/ModListItem.test.tsx +++ b/src/pageEditor/sidebar/ModListItem.test.tsx @@ -23,7 +23,7 @@ import { render } from "@/pageEditor/testHelpers"; import { Accordion, ListGroup } from "react-bootstrap"; import { appApiMock } from "@/testUtils/appApiMock"; import { modDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; describe("ModListItem", () => { it("renders expanded", async () => { @@ -99,7 +99,7 @@ describe("ModListItem", () => { const modDefinition = modDefinitionFactory({ metadata: { ...modMetadata, - version: validateSemVerString("1.0.1"), + version: normalizeSemVerString("1.0.1"), }, }); appApiMock diff --git a/src/pageEditor/sidebar/modals/CreateModModal.tsx b/src/pageEditor/sidebar/modals/CreateModModal.tsx index d0ca20756b..f2f6006e6d 100644 --- a/src/pageEditor/sidebar/modals/CreateModModal.tsx +++ b/src/pageEditor/sidebar/modals/CreateModModal.tsx @@ -20,7 +20,7 @@ import { PACKAGE_REGEX, testIsSemVerString, validateRegistryId, - validateSemVerString, + normalizeSemVerString, } from "@/types/helpers"; import { useDispatch, useSelector } from "react-redux"; import { @@ -88,7 +88,7 @@ function useInitialFormState({ return { id: newModId, name: `${modMetadata.name} (Copy)`, - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: modMetadata.description, }; } @@ -98,7 +98,7 @@ function useInitialFormState({ return { id: generatePackageId(scope, activeElement.label), name: activeElement.label, - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Created with the PixieBrix Page Editor", }; } diff --git a/src/pageEditor/starterBricks/base.ts b/src/pageEditor/starterBricks/base.ts index 11dc13c687..3a92333891 100644 --- a/src/pageEditor/starterBricks/base.ts +++ b/src/pageEditor/starterBricks/base.ts @@ -34,7 +34,7 @@ import { isInnerDefinitionRegistryId, uuidv4, validateRegistryId, - validateSemVerString, + normalizeSemVerString, } from "@/types/helpers"; import { type BrickPipeline, @@ -316,7 +316,7 @@ export function baseSelectExtensionPoint( id: metadata.id, // The server requires the version to save the brick, even though it's not marked as required // in the front-end schemas - version: metadata.version ?? validateSemVerString("1.0.0"), + version: metadata.version ?? normalizeSemVerString("1.0.0"), name: metadata.name, // The server requires the description to save the brick, even though it's not marked as required // in the front-end schemas diff --git a/src/platform/platformContext.ts b/src/platform/platformContext.ts index 1ab1695704..2a2c66bcae 100644 --- a/src/platform/platformContext.ts +++ b/src/platform/platformContext.ts @@ -21,14 +21,14 @@ import { PlatformCapabilityNotAvailableError, } from "@/platform/capabilities"; import { PlatformBase } from "@/platform/platformBase"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; /** * A platform protocol with no available capabilities. */ export const uninitializedPlatform = new PlatformBase( "uninitialized", - validateSemVerString("0.0.0"), + normalizeSemVerString("0.0.0"), ); /** diff --git a/src/registry/packageRegistry.test.ts b/src/registry/packageRegistry.test.ts index cfd60d5bfb..4cfeee83d1 100644 --- a/src/registry/packageRegistry.test.ts +++ b/src/registry/packageRegistry.test.ts @@ -26,7 +26,7 @@ import { produce } from "immer"; import { appApiMock } from "@/testUtils/appApiMock"; import { defaultModDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories"; import pDefer from "p-defer"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; describe("localRegistry", () => { beforeEach(() => { @@ -65,7 +65,7 @@ describe("localRegistry", () => { it("should return latest version", async () => { const definition = defaultModDefinitionFactory(); const updated = produce(definition, (draft) => { - draft.metadata.version = validateSemVerString("9.9.9"); + draft.metadata.version = normalizeSemVerString("9.9.9"); }); appApiMock.onGet("/api/registry/bricks/").reply(200, [updated, definition]); diff --git a/src/runtime/pipelineTests/component.test.ts b/src/runtime/pipelineTests/component.test.ts index 76b799d48f..21f919dd8d 100644 --- a/src/runtime/pipelineTests/component.test.ts +++ b/src/runtime/pipelineTests/component.test.ts @@ -26,7 +26,7 @@ import { } from "./pipelineTestHelpers"; import { fromJS } from "@/bricks/transformers/brickFactory"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; import { TEST_setContext } from "webext-detect-page"; import { toExpression } from "@/utils/expressionUtils"; @@ -43,7 +43,7 @@ const componentBlock = fromJS(blockRegistry, { metadata: { id: "test/component", name: "Component Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Component block using v1 runtime", }, inputSchema: { diff --git a/src/testUtils/factories/brickFactories.ts b/src/testUtils/factories/brickFactories.ts index c5e3696fe1..7f1f9110f9 100644 --- a/src/testUtils/factories/brickFactories.ts +++ b/src/testUtils/factories/brickFactories.ts @@ -23,7 +23,7 @@ import { registryIdSequence, uuidSequence, } from "@/testUtils/factories/stringFactories"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; import { emptyPermissionsFactory } from "@/permissions/permissionsUtils"; import { minimalSchemaFactory } from "@/utils/schemaUtils"; import type { BrickDefinition } from "@/bricks/transformers/brickFactory"; @@ -35,7 +35,7 @@ import type { ModDefinition } from "@/types/modDefinitionTypes"; export const brickFactory = define({ id: registryIdSequence, name: derive((x: Brick) => `Brick ${x.id}`, "id"), - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), inputSchema: minimalSchemaFactory, permissions: emptyPermissionsFactory, run: jest.fn(), diff --git a/src/testUtils/factories/deploymentFactories.ts b/src/testUtils/factories/deploymentFactories.ts index 0f57a116c5..2e95bc5d12 100644 --- a/src/testUtils/factories/deploymentFactories.ts +++ b/src/testUtils/factories/deploymentFactories.ts @@ -21,7 +21,7 @@ import { type ActivatableDeployment } from "@/types/deploymentTypes"; import { uuidSequence } from "@/testUtils/factories/stringFactories"; import { validateRegistryId, - validateSemVerString, + normalizeSemVerString, validateTimestamp, } from "@/types/helpers"; import { defaultModDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories"; @@ -32,7 +32,7 @@ import { type ModDefinition } from "@/types/modDefinitionTypes"; const deploymentPackageFactory = define({ id: uuidSequence, name: "Test Starter Brick", - version: (n: number) => validateSemVerString(`1.0.${n}`), + version: (n: number) => normalizeSemVerString(`1.0.${n}`), package_id: (n: number) => validateRegistryId(`test/starter-brick-${n}`), }); diff --git a/src/testUtils/factories/metadataFactory.ts b/src/testUtils/factories/metadataFactory.ts index b3a12c2792..54a603e57c 100644 --- a/src/testUtils/factories/metadataFactory.ts +++ b/src/testUtils/factories/metadataFactory.ts @@ -17,11 +17,11 @@ import { define } from "cooky-cutter"; import { type Metadata } from "@/types/registryTypes"; -import { validateRegistryId, validateSemVerString } from "@/types/helpers"; +import { validateRegistryId, normalizeSemVerString } from "@/types/helpers"; export const metadataFactory = define({ id: (n: number) => validateRegistryId(`test/mod-${n}`), name: (n: number) => `Mod ${n}`, description: "Mod generated from factory", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }); diff --git a/src/testUtils/platformMock.ts b/src/testUtils/platformMock.ts index 2a0f70b9b7..15a25d778b 100644 --- a/src/testUtils/platformMock.ts +++ b/src/testUtils/platformMock.ts @@ -21,7 +21,7 @@ import ConsoleLogger from "@/utils/ConsoleLogger"; import type { Logger } from "@/types/loggerTypes"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import type { RunArgs } from "@/types/runtimeTypes"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; import type { ToastProtocol } from "@/platform/platformTypes/toastProtocol"; /** @@ -29,7 +29,7 @@ import type { ToastProtocol } from "@/platform/platformTypes/toastProtocol"; */ export const platformMock: PlatformProtocol = { platformName: "mock", - version: validateSemVerString("0.0.0"), + version: normalizeSemVerString("0.0.0"), capabilities: platformCapabilities, open: jest.fn(), alert: jest.fn(), diff --git a/src/types/helpers.test.ts b/src/types/helpers.test.ts index 04dee28fe1..2750448fe2 100644 --- a/src/types/helpers.test.ts +++ b/src/types/helpers.test.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { testIsSemVerString, validateSemVerString } from "@/types/helpers"; +import { testIsSemVerString, normalizeSemVerString } from "@/types/helpers"; describe("types/helpers.ts", () => { describe("testIsSemVerString", () => { @@ -63,7 +63,7 @@ describe("types/helpers.ts", () => { ); }); - describe("validateSemVerString", () => { + describe("normalizeSemVerString", () => { it.each([ { value: "1.2.3", @@ -92,7 +92,7 @@ describe("types/helpers.ts", () => { ])( "$value with allowLeadingV: $allowLeadingV and coerce: $coerce returns $expected", ({ value, allowLeadingV, coerce, expected }) => { - expect(validateSemVerString(value, { allowLeadingV, coerce })).toBe( + expect(normalizeSemVerString(value, { allowLeadingV, coerce })).toBe( expected, ); }, @@ -123,7 +123,7 @@ describe("types/helpers.ts", () => { "$value with allowLeadingV: $allowLeadingV and coerce: $coerce throws an error", ({ value, allowLeadingV, coerce }) => { expect(() => - validateSemVerString(value, { allowLeadingV, coerce }), + normalizeSemVerString(value, { allowLeadingV, coerce }), ).toThrow(); }, ); diff --git a/src/types/helpers.ts b/src/types/helpers.ts index f36ca00cec..dc528ad378 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -151,7 +151,7 @@ export function validateTimestamp(value: string): Timestamp { throw new TypeError("Invalid timestamp"); } -export function validateSemVerString( +export function normalizeSemVerString( value: string, // Default to `false` to be stricter. { diff --git a/src/utils/deploymentUtils.test.ts b/src/utils/deploymentUtils.test.ts index ba4f492edb..55ce6ba8d8 100644 --- a/src/utils/deploymentUtils.test.ts +++ b/src/utils/deploymentUtils.test.ts @@ -25,7 +25,7 @@ import { import { uuidv4, validateRegistryId, - validateSemVerString, + normalizeSemVerString, validateTimestamp, } from "@/types/helpers"; import { type SanitizedIntegrationConfig } from "@/integrations/integrationTypes"; @@ -123,7 +123,7 @@ describe("makeUpdatedFilter", () => { _recipe: { ...modDefinition.metadata, // The factory produces version "1.0.1" - version: validateSemVerString("1.0.1"), + version: normalizeSemVerString("1.0.1"), updated_at: validateTimestamp(deployment.updated_at), // `sharing` doesn't impact the predicate. Pass an arbitrary value sharing: undefined, diff --git a/src/utils/extensionUtils.ts b/src/utils/extensionUtils.ts index b4e84bf263..5d5ca6dea2 100644 --- a/src/utils/extensionUtils.ts +++ b/src/utils/extensionUtils.ts @@ -20,7 +20,7 @@ import { foreverPendingPromise } from "@/utils/promiseUtils"; import { type Promisable } from "type-fest"; import { isScriptableUrl } from "webext-content-scripts"; import { type Runtime } from "webextension-polyfill"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; import { type SemVerString } from "@/types/registryTypes"; export const SHORTCUTS_URL = "chrome://extensions/shortcuts"; @@ -69,7 +69,7 @@ export function getExtensionConsoleUrl(page?: string): string { // TODO: Add linting rule to prefer getExtensionVersion over browser.runtime.getManifest().version export function getExtensionVersion(): SemVerString { - return validateSemVerString(browser.runtime.getManifest().version, { + return normalizeSemVerString(browser.runtime.getManifest().version, { coerce: true, }); } diff --git a/src/utils/objToYaml.test.ts b/src/utils/objToYaml.test.ts index 3ff5a8456d..08a420e768 100644 --- a/src/utils/objToYaml.test.ts +++ b/src/utils/objToYaml.test.ts @@ -16,7 +16,7 @@ */ import { brickToYaml } from "./objToYaml"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; describe("brickToYaml", () => { test("serializes arbitrary object", () => { @@ -40,7 +40,7 @@ lorem: ipsum metadata: { id: "google/api", name: "Google API", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Generic Google API authentication via API key", }, apiVersion: "v1", @@ -104,7 +104,7 @@ outputSchema: metadata: { id: "google/api", name: "Google API", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Generic Google API authentication via API key", }, apiVersion: "v1", From 75415e6032975d3518ed62dcca8155cd105d28c2 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 12:08:40 -0500 Subject: [PATCH 14/16] add documentation for getExtensionVersion --- src/utils/extensionUtils.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/utils/extensionUtils.ts b/src/utils/extensionUtils.ts index 5d5ca6dea2..69faeda8fc 100644 --- a/src/utils/extensionUtils.ts +++ b/src/utils/extensionUtils.ts @@ -67,7 +67,19 @@ export function getExtensionConsoleUrl(page?: string): string { return url.href; } -// TODO: Add linting rule to prefer getExtensionVersion over browser.runtime.getManifest().version +/** + * Gets the Extension version from the manifest and normalizes it to a valid semver string. + * @since 1.8.13, the Extension version is a four part format x.x.x.x + * This allows us to publish pre-release versions to the CWS, especially the BETA listing + * Each version published in CWS must have a unique version number + * + * @see manifest.mjs:getVersion() + * + * TODO: Add linting rule to prefer getExtensionVersion over browser.runtime.getManifest().version + * @see https://github.com/pixiebrix/pixiebrix-extension/issues/8349 + * + * @returns the version of the Extension in valid semver format (x.x.x) + */ export function getExtensionVersion(): SemVerString { return normalizeSemVerString(browser.runtime.getManifest().version, { coerce: true, From 46ed4af90a1b504e1b160fc248bcf74a6059ddc7 Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 12:12:32 -0500 Subject: [PATCH 15/16] test bad values --- src/types/helpers.test.ts | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/types/helpers.test.ts b/src/types/helpers.test.ts index 2750448fe2..a52101d95b 100644 --- a/src/types/helpers.test.ts +++ b/src/types/helpers.test.ts @@ -53,6 +53,42 @@ describe("types/helpers.ts", () => { coerce: true, expected: true, }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: false, + expected: false, + }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: true, + expected: false, + }, + { + value: "", + allowLeadingV: false, + coerce: false, + expected: false, + }, + { + value: "", + allowLeadingV: false, + coerce: true, + expected: false, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: false, + expected: false, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: true, + expected: false, + }, ])( "$value with allowLeadingV: $allowLeadingV and coerce: $coerce returns $expected", ({ value, allowLeadingV, coerce, expected }) => { @@ -119,6 +155,36 @@ describe("types/helpers.ts", () => { allowLeadingV: true, coerce: false, }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: false, + }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: true, + }, + { + value: "", + allowLeadingV: false, + coerce: false, + }, + { + value: "", + allowLeadingV: false, + coerce: true, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: false, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: true, + }, ])( "$value with allowLeadingV: $allowLeadingV and coerce: $coerce throws an error", ({ value, allowLeadingV, coerce }) => { From ecc7904ad5e8124816457de3dd143e8ecda38cab Mon Sep 17 00:00:00 2001 From: Graham Langford Date: Thu, 25 Apr 2024 12:15:44 -0500 Subject: [PATCH 16/16] js docs for normalizeSemVerString --- src/types/helpers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types/helpers.ts b/src/types/helpers.ts index dc528ad378..8f815b4945 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -151,6 +151,12 @@ export function validateTimestamp(value: string): Timestamp { throw new TypeError("Invalid timestamp"); } +/** + * @param value The string to normalize + * @param allowLeadingV If `true`, a leading `v` is allowed. This results in a semver string that is not actually valid + * @param coerce If `true`, the string will be coerced to a valid semver string. See https://www.npmjs.com/package/semver#coercion + * @returns A normalized semver string + */ export function normalizeSemVerString( value: string, // Default to `false` to be stricter.