|
1 | 1 | import {currentProcessIsGlobal, inferPackageManagerForGlobalCLI, installGlobalCLIPrompt} from './is-global.js' |
| 2 | +import {cwd} from './path.js' |
2 | 3 | import {terminalSupportsPrompting} from './system.js' |
3 | 4 | import {renderSelectPrompt} from './ui.js' |
4 | 5 | import {globalCLIVersion} from './version.js' |
5 | 6 | import * as execa from 'execa' |
6 | 7 | import {beforeEach, describe, expect, test, vi} from 'vitest' |
| 8 | +import {realpathSync} from 'fs' |
7 | 9 |
|
8 | 10 | vi.mock('./system.js') |
9 | 11 | vi.mock('./ui.js') |
10 | 12 | vi.mock('execa') |
11 | 13 | vi.mock('which') |
12 | 14 | vi.mock('./version.js') |
13 | 15 |
|
| 16 | +// Mock fs.realpathSync at the module level |
| 17 | +vi.mock('fs', async (importOriginal) => { |
| 18 | + const actual = await importOriginal<typeof import('fs')>() |
| 19 | + return { |
| 20 | + ...actual, |
| 21 | + realpathSync: vi.fn((path: string) => path), |
| 22 | + } |
| 23 | +}) |
| 24 | + |
14 | 25 | const globalNPMPath = '/path/to/global/npm' |
15 | 26 | const globalYarnPath = '/path/to/global/yarn' |
16 | 27 | const globalPNPMPath = '/path/to/global/pnpm' |
| 28 | +const globalHomebrewIntel = '/usr/local/Cellar/shopify-cli/3.89.0/bin/shopify' |
| 29 | +const globalHomebrewAppleSilicon = '/opt/homebrew/Cellar/shopify-cli/3.89.0/bin/shopify' |
| 30 | +const globalHomebrewLinux = '/home/linuxbrew/.linuxbrew/Cellar/shopify-cli/3.89.0/bin/shopify' |
17 | 31 | const unknownGlobalPath = '/path/to/global/unknown' |
18 | | -const localProjectPath = '/path/local' |
| 32 | +// Must be within the actual workspace so currentProcessIsGlobal recognizes it as local |
| 33 | +const localProjectPath = `${cwd()}/node_modules/.bin/shopify` |
19 | 34 |
|
20 | 35 | beforeEach(() => { |
21 | 36 | ;(vi.mocked(execa.execaSync) as any).mockReturnValue({stdout: localProjectPath}) |
@@ -46,6 +61,12 @@ describe('currentProcessIsGlobal', () => { |
46 | 61 | }) |
47 | 62 |
|
48 | 63 | describe('inferPackageManagerForGlobalCLI', () => { |
| 64 | + beforeEach(() => { |
| 65 | + // Reset mock to return the input path by default (no symlink resolution) |
| 66 | + vi.mocked(realpathSync).mockClear() |
| 67 | + vi.mocked(realpathSync).mockImplementation((path) => String(path)) |
| 68 | + }) |
| 69 | + |
49 | 70 | test('returns yarn if yarn is in path', async () => { |
50 | 71 | // Given |
51 | 72 | const argv = ['node', globalYarnPath, 'shopify'] |
@@ -89,6 +110,115 @@ describe('inferPackageManagerForGlobalCLI', () => { |
89 | 110 | // Then |
90 | 111 | expect(got).toBe('unknown') |
91 | 112 | }) |
| 113 | + |
| 114 | + test('returns homebrew if SHOPIFY_HOMEBREW_FORMULA is set', async () => { |
| 115 | + // Given |
| 116 | + const argv = ['node', globalHomebrewAppleSilicon, 'shopify'] |
| 117 | + const env = {SHOPIFY_HOMEBREW_FORMULA: 'shopify-cli'} |
| 118 | + |
| 119 | + // When |
| 120 | + const got = inferPackageManagerForGlobalCLI(argv, env) |
| 121 | + |
| 122 | + // Then |
| 123 | + expect(got).toBe('homebrew') |
| 124 | + }) |
| 125 | + |
| 126 | + test('returns homebrew for Intel Mac Cellar path', async () => { |
| 127 | + // Given |
| 128 | + const argv = ['node', globalHomebrewIntel, 'shopify'] |
| 129 | + |
| 130 | + // When |
| 131 | + const got = inferPackageManagerForGlobalCLI(argv) |
| 132 | + |
| 133 | + // Then |
| 134 | + expect(got).toBe('homebrew') |
| 135 | + }) |
| 136 | + |
| 137 | + test('returns homebrew for Apple Silicon Cellar path', async () => { |
| 138 | + // Given |
| 139 | + const argv = ['node', globalHomebrewAppleSilicon, 'shopify'] |
| 140 | + |
| 141 | + // When |
| 142 | + const got = inferPackageManagerForGlobalCLI(argv) |
| 143 | + |
| 144 | + // Then |
| 145 | + expect(got).toBe('homebrew') |
| 146 | + }) |
| 147 | + |
| 148 | + test('returns homebrew for Linux Homebrew path', async () => { |
| 149 | + // Given |
| 150 | + const argv = ['node', globalHomebrewLinux, 'shopify'] |
| 151 | + |
| 152 | + // When |
| 153 | + const got = inferPackageManagerForGlobalCLI(argv) |
| 154 | + |
| 155 | + // Then |
| 156 | + expect(got).toBe('homebrew') |
| 157 | + }) |
| 158 | + |
| 159 | + test('returns homebrew when HOMEBREW_PREFIX matches path', async () => { |
| 160 | + // Given |
| 161 | + const argv = ['node', '/opt/homebrew/bin/shopify', 'shopify'] |
| 162 | + const env = {HOMEBREW_PREFIX: '/opt/homebrew'} |
| 163 | + |
| 164 | + // When |
| 165 | + const got = inferPackageManagerForGlobalCLI(argv, env) |
| 166 | + |
| 167 | + // Then |
| 168 | + expect(got).toBe('homebrew') |
| 169 | + }) |
| 170 | + |
| 171 | + test('resolves symlinks to detect actual package manager (yarn)', async () => { |
| 172 | + // Given: A symlink in /opt/homebrew/bin pointing to yarn global |
| 173 | + const symlinkPath = '/opt/homebrew/bin/shopify' |
| 174 | + const realYarnPath = '/Users/user/.config/yarn/global/node_modules/.bin/shopify' |
| 175 | + const argv = ['node', symlinkPath, 'shopify'] |
| 176 | + const env = {HOMEBREW_PREFIX: '/opt/homebrew'} |
| 177 | + |
| 178 | + // Mock realpathSync to resolve the symlink |
| 179 | + vi.mocked(realpathSync).mockReturnValueOnce(realYarnPath) |
| 180 | + |
| 181 | + // When |
| 182 | + const got = inferPackageManagerForGlobalCLI(argv, env) |
| 183 | + |
| 184 | + // Then: Should detect yarn (from real path), not homebrew (from symlink) |
| 185 | + expect(got).toBe('yarn') |
| 186 | + expect(vi.mocked(realpathSync)).toHaveBeenCalledWith(symlinkPath) |
| 187 | + }) |
| 188 | + |
| 189 | + test('resolves symlinks to detect real homebrew installation', async () => { |
| 190 | + // Given: A symlink in /opt/homebrew/bin pointing to a Cellar path (real Homebrew) |
| 191 | + const symlinkPath = '/opt/homebrew/bin/shopify' |
| 192 | + const realHomebrewPath = '/opt/homebrew/Cellar/shopify-cli/3.89.0/bin/shopify' |
| 193 | + const argv = ['node', symlinkPath, 'shopify'] |
| 194 | + |
| 195 | + // Mock realpathSync to resolve the symlink |
| 196 | + vi.mocked(realpathSync).mockReturnValueOnce(realHomebrewPath) |
| 197 | + |
| 198 | + // When |
| 199 | + const got = inferPackageManagerForGlobalCLI(argv) |
| 200 | + |
| 201 | + // Then: Should still detect homebrew from the real Cellar path |
| 202 | + expect(got).toBe('homebrew') |
| 203 | + }) |
| 204 | + |
| 205 | + test('falls back to original path if realpath fails', async () => { |
| 206 | + // Given: A path that realpathSync cannot resolve |
| 207 | + const nonExistentPath = '/opt/homebrew/bin/shopify' |
| 208 | + const argv = ['node', nonExistentPath, 'shopify'] |
| 209 | + const env = {HOMEBREW_PREFIX: '/opt/homebrew'} |
| 210 | + |
| 211 | + // Mock realpathSync to throw an error |
| 212 | + vi.mocked(realpathSync).mockImplementationOnce(() => { |
| 213 | + throw new Error('ENOENT: no such file or directory') |
| 214 | + }) |
| 215 | + |
| 216 | + // When |
| 217 | + const got = inferPackageManagerForGlobalCLI(argv, env) |
| 218 | + |
| 219 | + // Then: Should fall back to checking the original path |
| 220 | + expect(got).toBe('homebrew') |
| 221 | + }) |
92 | 222 | }) |
93 | 223 |
|
94 | 224 | describe('installGlobalCLIPrompt', () => { |
|
0 commit comments