diff --git a/.changeset/moody-beds-sneeze.md b/.changeset/moody-beds-sneeze.md new file mode 100644 index 0000000000..876b06cad6 --- /dev/null +++ b/.changeset/moody-beds-sneeze.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli-kit': patch +'@shopify/app': patch +--- + +Show a warning when there are multiple CLI installations diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index 426c446728..584a8c08f5 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -27,6 +27,7 @@ import { pnpmLockfile, PackageJson, pnpmWorkspaceFile, + localCLIVersion, } from '@shopify/cli-kit/node/node-package-manager' import {inTemporaryDirectory, moveFile, mkdir, mkTmpDir, rmdir, writeFile} from '@shopify/cli-kit/node/fs' import {joinPath, dirname, cwd, normalizePath} from '@shopify/cli-kit/node/path' @@ -34,14 +35,17 @@ import {platformAndArch} from '@shopify/cli-kit/node/os' import {outputContent, outputToken} from '@shopify/cli-kit/node/output' import {zod} from '@shopify/cli-kit/node/schema' import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' -import {currentProcessIsGlobal} from '@shopify/cli-kit/node/is-global' import colors from '@shopify/cli-kit/node/colors' -// eslint-disable-next-line no-restricted-imports -import {resolve} from 'path' + +import {globalCLIVersion, isGlobalCLIInstalled} from '@shopify/cli-kit/node/is-global' vi.mock('../../services/local-storage.js') vi.mock('../../services/app/config/use.js') vi.mock('@shopify/cli-kit/node/is-global') +vi.mock('@shopify/cli-kit/node/node-package-manager', async () => ({ + ...((await vi.importActual('@shopify/cli-kit/node/node-package-manager')) as any), + localCLIVersion: vi.fn(), +})) describe('load', () => { let specifications: ExtensionSpecification[] = [] @@ -322,9 +326,10 @@ wrong = "property" test('shows warning if using global CLI but app has local dependency', async () => { // Given - vi.mocked(currentProcessIsGlobal).mockReturnValueOnce(true) + vi.mocked(isGlobalCLIInstalled).mockResolvedValue(true) + vi.mocked(globalCLIVersion).mockResolvedValue('3.68.0') + vi.mocked(localCLIVersion).mockResolvedValue('3.65.0') const mockOutput = mockAndCaptureOutput() - mockOutput.clear() await writeConfig(appConfiguration, { workspaces: ['packages/*'], name: 'my_app', @@ -339,16 +344,19 @@ wrong = "property" expect(mockOutput.info()).toMatchInlineSnapshot(` "╭─ info ───────────────────────────────────────────────────────────────────────╮ │ │ - │ You are running a global installation of Shopify CLI │ + │ Two Shopify CLI installations found – using local dependency │ │ │ - │ This project has Shopify CLI as a local dependency in package.json. If you │ - │ prefer to use that version, run the command with your package manager │ - │ (e.g. npm run shopify). │ + │ A global installation (v3.68.0) and a local dependency (v3.65.0) were │ + │ detected. │ + │ We recommend removing the @shopify/cli and @shopify/app dependencies from │ + │ your package.json, unless you want to use different versions across │ + │ multiple apps. │ │ │ │ For more information, see Shopify CLI documentation [1] │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ - [1] https://shopify.dev/docs/apps/tools/cli + [1] https://shopify.dev/docs/apps/build/cli-for-apps#switch-to-a-global-executab + le-or-local-dependency " `) mockOutput.clear() @@ -356,8 +364,8 @@ wrong = "property" test('doesnt show warning if there is no local dependency', async () => { // Given + vi.mocked(localCLIVersion).mockResolvedValue(undefined) const mockOutput = mockAndCaptureOutput() - mockOutput.clear() await writeConfig(appConfiguration, { workspaces: ['packages/*'], name: 'my_app', @@ -2325,7 +2333,7 @@ wrong = "property" // Then expect(use).toHaveBeenCalledWith({ - directory: normalizePath(resolve(tmpDir)), + directory: normalizePath(tmpDir), shouldRenderSuccess: false, warningContent: { headline: "Couldn't find shopify.app.non-existent.toml", diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index f834849d2c..01c0ad42ea 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -37,6 +37,7 @@ import { getPackageManager, getPackageName, usesWorkspaces as appUsesWorkspaces, + localCLIVersion, } from '@shopify/cli-kit/node/node-package-manager' import {resolveFramework} from '@shopify/cli-kit/node/framework' import {hashString} from '@shopify/cli-kit/node/crypto' @@ -48,7 +49,7 @@ import {joinWithAnd, slugify} from '@shopify/cli-kit/common/string' import {getArrayRejectingUndefined} from '@shopify/cli-kit/common/array' import {checkIfIgnoredInGitRepository} from '@shopify/cli-kit/node/git' import {renderInfo} from '@shopify/cli-kit/node/ui' -import {currentProcessIsGlobal} from '@shopify/cli-kit/node/is-global' +import {currentProcessIsGlobal, globalCLIVersion} from '@shopify/cli-kit/node/is-global' const defaultExtensionDirectory = 'extensions/*' @@ -304,7 +305,7 @@ class AppLoader { } } +/** + * Returns true if the global CLI is installed. + * + * @returns `true` if the global CLI is installed. + */ +export async function globalCLIVersion(): Promise { + try { + if (!(await isGlobalCLIInstalled())) return undefined + return captureOutput('shopify', ['version']) + // eslint-disable-next-line no-catch-all/no-catch-all + } catch { + return undefined + } +} + /** * Installs the global Shopify CLI, using the provided package manager. * diff --git a/packages/cli-kit/src/public/node/node-package-manager.test.ts b/packages/cli-kit/src/public/node/node-package-manager.test.ts index 2f8213ecff..8599e993e9 100644 --- a/packages/cli-kit/src/public/node/node-package-manager.test.ts +++ b/packages/cli-kit/src/public/node/node-package-manager.test.ts @@ -20,6 +20,7 @@ import { checkForCachedNewVersion, inferPackageManager, PackageManager, + localCLIVersion, } from './node-package-manager.js' import {captureOutput, exec} from './system.js' import {inTemporaryDirectory, mkdir, touchFile, writeFile} from './fs.js' @@ -1027,3 +1028,33 @@ describe('inferPackageManager', () => { expect(inferPackageManager(undefined, mockEnv)).toBe('npm') }) }) + +describe('localCLIVersion', () => { + test('returns the version of the local CLI', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.mocked(captureOutput).mockResolvedValueOnce(`folder@ ${tmpDir} +└── @shopify/cli@3.68.0`) + + // When + const got = await localCLIVersion(tmpDir) + + // Then + expect(got).toEqual('3.68.0') + }) + }) + + test('returns undefined when the dependency is not installed', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.mocked(captureOutput).mockResolvedValueOnce(`folder@ ${tmpDir} + └── (empty)`) + + // When + const got = await localCLIVersion(tmpDir) + + // Then + expect(got).toBeUndefined() + }) + }) +}) diff --git a/packages/cli-kit/src/public/node/node-package-manager.ts b/packages/cli-kit/src/public/node/node-package-manager.ts index 118998f77a..828dcfa106 100644 --- a/packages/cli-kit/src/public/node/node-package-manager.ts +++ b/packages/cli-kit/src/public/node/node-package-manager.ts @@ -732,3 +732,19 @@ export function inferPackageManager(optionsPackageManager: string | undefined, e return 'npm' } + +/** + * Returns the version of the local dependency of the CLI if it's installed in the provided directory. + * + * @param directory - path of the project to look for the dependency. + * @returns the CLI version or undefined if the dependency is not installed. + */ +export async function localCLIVersion(directory: string): Promise { + try { + const output = await captureOutput('npm', ['list', '@shopify/cli'], {cwd: directory}) + return output.match(/@shopify\/cli@([\w.-]*)/)?.[1] + // eslint-disable-next-line no-catch-all/no-catch-all + } catch { + return undefined + } +}