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.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/scripts/manifest.test.js b/scripts/manifest.test.js
index 3d71e281e1..b61212e83c 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,76 @@ 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("four digit 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"]);
+ });
+
+ 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"]);
+ });
});
});
diff --git a/src/background/backgroundPlatform.ts b/src/background/backgroundPlatform.ts
index 2ec60d2f57..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,10 +40,7 @@ class BackgroundPlatform extends PlatformBase {
});
constructor() {
- super(
- "background",
- validateSemVerString(browser.runtime.getManifest().version),
- );
+ super("background", getExtensionVersion());
}
override get logger(): PlatformProtocol["logger"] {
diff --git a/src/background/deploymentUpdater.test.ts b/src/background/deploymentUpdater.test.ts
index 1d5fcff8a4..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";
@@ -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
@@ -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/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..6b519a90c8 100644
--- a/src/background/messenger/external/api.ts
+++ b/src/background/messenger/external/api.ts
@@ -23,9 +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 () =>
- browser.runtime.getManifest(),
+ // Ensure the version we send to the app is a valid semver.
+ ({
+ ...browser.runtime.getManifest(),
+ version: getExtensionVersion(),
+ }),
);
export const setExtensionAuth = liftExternal(
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(),
}));
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 c15915ebbc..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,10 +91,7 @@ class ContentScriptPlatform extends PlatformBase {
});
constructor() {
- super(
- "contentScript",
- validateSemVerString(browser.runtime.getManifest().version),
- );
+ super("contentScript", getExtensionVersion());
}
override capabilities: PlatformCapability[] = [
diff --git a/src/data/service/errorService.ts b/src/data/service/errorService.ts
index 7c0e464bd8..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);
+ 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/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/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 fc4b37844e..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,10 +56,7 @@ class ExtensionPagePlatform extends PlatformBase {
});
constructor() {
- super(
- "extension",
- validateSemVerString(browser.runtime.getManifest().version),
- );
+ super("extension", getExtensionVersion());
}
override alert = window.alert;
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/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/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",
diff --git a/src/types/helpers.test.ts b/src/types/helpers.test.ts
new file mode 100644
index 0000000000..a52101d95b
--- /dev/null
+++ b/src/types/helpers.test.ts
@@ -0,0 +1,197 @@
+/*
+ * 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, normalizeSemVerString } 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.4000",
+ allowLeadingV: false,
+ coerce: false,
+ expected: false,
+ },
+ {
+ value: "1.2.3.4000",
+ allowLeadingV: false,
+ coerce: true,
+ expected: true,
+ },
+ {
+ value: "v1.2.3.4000",
+ allowLeadingV: true,
+ 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 }) => {
+ expect(testIsSemVerString(value, { allowLeadingV, coerce })).toBe(
+ expected,
+ );
+ },
+ );
+ });
+
+ describe("normalizeSemVerString", () => {
+ 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.4000",
+ allowLeadingV: false,
+ coerce: true,
+ expected: "1.2.3",
+ },
+ {
+ value: "v1.2.3.4000",
+ allowLeadingV: true,
+ coerce: true,
+ expected: "v1.2.3",
+ },
+ ])(
+ "$value with allowLeadingV: $allowLeadingV and coerce: $coerce returns $expected",
+ ({ value, allowLeadingV, coerce, expected }) => {
+ expect(normalizeSemVerString(value, { allowLeadingV, coerce })).toBe(
+ expected,
+ );
+ },
+ );
+
+ it.each([
+ {
+ value: "v1.2.3",
+ allowLeadingV: false,
+ coerce: false,
+ },
+ {
+ value: "1.2.3.4000",
+ allowLeadingV: false,
+ coerce: false,
+ },
+ {
+ value: "v1.2.3.4000",
+ allowLeadingV: false,
+ coerce: true,
+ },
+ {
+ value: "v1.2.3.4000",
+ 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 }) => {
+ expect(() =>
+ normalizeSemVerString(value, { allowLeadingV, coerce }),
+ ).toThrow();
+ },
+ );
+ });
+});
diff --git a/src/types/helpers.ts b/src/types/helpers.ts
index a1776d8478..8f815b4945 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 {
@@ -151,10 +151,19 @@ export function validateTimestamp(value: string): Timestamp {
throw new TypeError("Invalid timestamp");
}
-export function validateSemVerString(
+/**
+ * @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.
- { 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 +171,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 +193,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");
}
diff --git a/src/utils/deploymentUtils.test.ts b/src/utils/deploymentUtils.test.ts
index 2c97242627..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";
@@ -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 }]])(
@@ -122,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,
@@ -151,9 +152,8 @@ describe("checkExtensionUpdateRequired", () => {
test("update not required", () => {
const { deployment, modDefinition } = activatableDeploymentFactory();
- (modDefinition.metadata.extensionVersion as any) = `>=${
- browser.runtime.getManifest().version
- }`;
+ (modDefinition.metadata.extensionVersion as any) =
+ `>=${getExtensionVersion()}`;
expect(
checkExtensionUpdateRequired([{ deployment, modDefinition }]),
diff --git a/src/utils/deploymentUtils.ts b/src/utils/deploymentUtils.ts
index 27300ce05e..cd32cc310b 100644
--- a/src/utils/deploymentUtils.ts
+++ b/src/utils/deploymentUtils.ts
@@ -30,6 +30,7 @@ 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,7 +125,8 @@ export function checkExtensionUpdateRequired(
activatableDeployments: ActivatableDeployment[] = [],
): boolean {
// Check that the user's extension can run the deployment
- const { version: extensionVersion } = browser.runtime.getManifest();
+
+ 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..69faeda8fc 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 { normalizeSemVerString } from "@/types/helpers";
+import { type SemVerString } from "@/types/registryTypes";
export const SHORTCUTS_URL = "chrome://extensions/shortcuts";
type Command = "toggle-quick-bar";
@@ -65,8 +67,23 @@ export function getExtensionConsoleUrl(page?: string): string {
return url.href;
}
-export function getExtensionVersion(): string {
- return 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,
+ });
}
/** If no update is available and downloaded yet, it will return a string explaining why */
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",