Skip to content

Commit

Permalink
Fix/widgets stale latest version (#2402)
Browse files Browse the repository at this point in the history
  • Loading branch information
mimi-imtbl authored Nov 18, 2024
1 parent cbfd9db commit b22471a
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 7 deletions.
3 changes: 2 additions & 1 deletion packages/checkout/sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ export class Checkout {
// Determine the version of the widgets to load
const validatedBuildVersion = validateAndBuildVersion(init.version);
const initVersionProvided = init.version !== undefined;
const widgetsVersion = determineWidgetsVersion(

const widgetsVersion = await determineWidgetsVersion(
validatedBuildVersion,
initVersionProvided,
versionConfig,
Expand Down
79 changes: 76 additions & 3 deletions packages/checkout/sdk/src/widgets/version.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SDK_VERSION_MARKER } from '../env';
import { CheckoutWidgetsVersionConfig } from '../types';
import { SemanticVersion } from './definitions/types';
import { determineWidgetsVersion, validateAndBuildVersion } from './version';
import { determineWidgetsVersion, getLatestVersionFromNpm, validateAndBuildVersion } from './version';

describe('CheckoutWidgets', () => {
const SDK_VERSION = SDK_VERSION_MARKER;
Expand Down Expand Up @@ -333,8 +333,8 @@ describe('CheckoutWidgets', () => {
];

determineWidgetVersionTestCases.forEach((testCase) => {
it(`should determine correct widget version when ${testCase.title}`, () => {
const widgetVersion = determineWidgetsVersion(
it(`should determine correct widget version when ${testCase.title}`, async () => {
const widgetVersion = await determineWidgetsVersion(
testCase.validatedBuildVersion,
testCase.initVersionProvided,
testCase?.checkoutVersionConfig,
Expand All @@ -343,4 +343,77 @@ describe('CheckoutWidgets', () => {
});
});
});

describe('Get Latest Version from NPM', () => {
beforeEach(() => {
global.fetch = jest.fn();
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should return the latest version when the fetch succeeds', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({
version: '1.82.3',
}),
});

const version = await getLatestVersionFromNpm();
expect(version).toBe('1.82.3');
});

it('should return "latest" if the response is not ok', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
json: async () => ({}),
});

const version = await getLatestVersionFromNpm();
expect(version).toBe('latest');
});

it('should return "latest" if the response has no "dist-tags"', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({}),
});

const version = await getLatestVersionFromNpm();
expect(version).toBe('latest');
});

it('should return "latest" if the "latest" tag is empty or missing', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({
'dist-tags': { latest: '' },
}),
});

const version = await getLatestVersionFromNpm();
expect(version).toBe('latest');
});

it('should return "latest" if fetch throws a network error', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network Error'));

const version = await getLatestVersionFromNpm();
expect(version).toBe('latest');
});

it('should return "latest" if the JSON response is invalid', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => {
throw new Error('Invalid JSON');
},
});

const version = await getLatestVersionFromNpm();
expect(version).toBe('latest');
});
});
});
47 changes: 44 additions & 3 deletions packages/checkout/sdk/src/widgets/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ export function validateAndBuildVersion(
return validatedVersion;
}

/**
* Fetches the latest version of the package from the NPM registry.
* Loads a specific latest version instead of relying on the latest tag helps with caching issues.
* Falls back to 'latest' if an error occurs or if the response is invalid.
* @returns {Promise<string>} A promise resolving to the latest version string or 'latest'.
*/
export async function getLatestVersionFromNpm(): Promise<string> {
const npmRegistryUrl = 'https://registry.npmjs.org/@imtbl/sdk/latest';
const fallbackVersion = 'latest';

try {
const response = await fetch(npmRegistryUrl);

if (!response.ok) {
return fallbackVersion;
}

const data = await response.json();
const version = data.version?.trim();

if (version) {
return version;
}

return fallbackVersion;
} catch (error) {
return fallbackVersion;
}
}

/**
* Returns the latest compatible version based on the provided checkout version config.
* If no compatible version markers are provided, it returns 'latest'.
Expand All @@ -76,12 +106,12 @@ function latestCompatibleVersion(
* If the build version is an alpha, it uses that version.
* Defaults to 'latest' if no compatible version markers are found.
*/
export function determineWidgetsVersion(
export async function determineWidgetsVersion(
validatedBuildVersion: string,
initVersionProvided: boolean,
versionConfig?: CheckoutWidgetsVersionConfig,
) {
// If version is provided in widget init parms, use that
// If version is provided in widget init params, use that
if (initVersionProvided) {
return validatedBuildVersion;
}
Expand All @@ -96,5 +126,16 @@ export function determineWidgetsVersion(
return validatedBuildVersion;
}

return latestCompatibleVersion(validatedBuildVersion, versionConfig.compatibleVersionMarkers);
const compatibleVersion = latestCompatibleVersion(
validatedBuildVersion,
versionConfig.compatibleVersionMarkers,
);

// If `latest` is returned, query NPM registry for the actual latest version
if (compatibleVersion === 'latest') {
const latestVersion = await getLatestVersionFromNpm();
return latestVersion;
}

return compatibleVersion;
}

0 comments on commit b22471a

Please sign in to comment.