diff --git a/packages/cli-kit/src/public/node/version.test.ts b/packages/cli-kit/src/public/node/version.test.ts index 98f3ce67a1c..1d6489c3239 100644 --- a/packages/cli-kit/src/public/node/version.test.ts +++ b/packages/cli-kit/src/public/node/version.test.ts @@ -1,7 +1,7 @@ import {captureOutput} from './system.js' -import {localCLIVersion, globalCLIVersion, isPreReleaseVersion} from './version.js' +import {localCLIVersion, globalCLIVersion, isPreReleaseVersion, _resetGlobalCLIVersionCache} from './version.js' import {inTemporaryDirectory} from './fs.js' -import {describe, expect, test, vi} from 'vitest' +import {describe, expect, test, vi, beforeEach} from 'vitest' import which from 'which' @@ -39,6 +39,10 @@ describe('localCLIVersion', () => { }) describe('globalCLIVersion', () => { + beforeEach(() => { + _resetGlobalCLIVersionCache() + }) + test('returns the version when a recent CLI is installed globally', async () => { // Given // TS is not detecting the return type correctly, so we need to cast it @@ -78,6 +82,35 @@ describe('globalCLIVersion', () => { expect(got).toBeUndefined() expect(captureOutput).not.toHaveBeenCalled() }) + + test('memoizes the result', async () => { + // Given + vi.mocked(which.sync).mockReturnValue(['path/to/shopify'] as unknown as string) + vi.mocked(captureOutput).mockResolvedValue('@shopify/cli/3.65.0') + + // When + const got1 = await globalCLIVersion() + const got2 = await globalCLIVersion() + + // Then + expect(got1).toBe('3.65.0') + expect(got2).toBe('3.65.0') + expect(captureOutput).toHaveBeenCalledTimes(1) + }) + + test('resets the cache when _resetGlobalCLIVersionCache is called', async () => { + // Given + vi.mocked(which.sync).mockReturnValue(['path/to/shopify'] as unknown as string) + vi.mocked(captureOutput).mockResolvedValue('@shopify/cli/3.65.0') + + // When + await globalCLIVersion() + _resetGlobalCLIVersionCache() + await globalCLIVersion() + + // Then + expect(captureOutput).toHaveBeenCalledTimes(2) + }) }) describe('isPreReleaseVersion', () => { diff --git a/packages/cli-kit/src/public/node/version.ts b/packages/cli-kit/src/public/node/version.ts index a28ba9e44cf..e6fcaaaf385 100644 --- a/packages/cli-kit/src/public/node/version.ts +++ b/packages/cli-kit/src/public/node/version.ts @@ -1,6 +1,12 @@ import {captureOutput} from './system.js' import which from 'which' import {satisfies, SemVer} from 'semver' + +/** + * Memoized value for the global CLI version. + */ +let memoizedGlobalCLIVersion: Promise | undefined + /** * Returns the version of the local dependency of the CLI if it's installed in the provided directory. * @@ -22,25 +28,40 @@ export async function localCLIVersion(directory: string): Promise { - try { - const env = {...process.env, SHOPIFY_CLI_NO_ANALYTICS: '1'} - // Both execa and which find the project dependency. We need to exclude it. - const shopifyBinaries = which.sync('shopify', {all: true}).filter((path) => !path.includes('node_modules')) - if (!shopifyBinaries[0]) return undefined - const output = await captureOutput(shopifyBinaries[0], [], {env}) - const versionMatch = output.match(/@shopify\/cli\/([^\s]+)/) - if (versionMatch && versionMatch[1]) { - const version = versionMatch[1] - if (satisfies(version, `>=3.59.0`) || isPreReleaseVersion(version)) { - return version +export function globalCLIVersion(): Promise { + if (memoizedGlobalCLIVersion) { + return memoizedGlobalCLIVersion + } + memoizedGlobalCLIVersion = (async () => { + try { + const env = {...process.env, SHOPIFY_CLI_NO_ANALYTICS: '1'} + // Both execa and which find the project dependency. We need to exclude it. + const shopifyBinaries = which.sync('shopify', {all: true}).filter((path) => !path.includes('node_modules')) + const binary = shopifyBinaries[0] + if (binary) { + const output = await captureOutput(binary, [], {env}) + const versionMatch = output.match(/@shopify\/cli\/([^\s]+)/) + if (versionMatch && versionMatch[1]) { + const version = versionMatch[1] + if (satisfies(version, `>=3.59.0`) || isPreReleaseVersion(version)) { + return version + } + } } + return undefined + // eslint-disable-next-line no-catch-all/no-catch-all + } catch { + return undefined } - return undefined - // eslint-disable-next-line no-catch-all/no-catch-all - } catch { - return undefined - } + })() + return memoizedGlobalCLIVersion +} + +/** + * Resets the memoized value for the global CLI version. + */ +export function _resetGlobalCLIVersionCache(): void { + memoizedGlobalCLIVersion = undefined } /**