1import spawnAsync from '@expo/spawn-async'; 2import { vol } from 'memfs'; 3import path from 'path'; 4 5import { mockSpawnPromise, mockedSpawnAsync, STUB_SPAWN_CHILD } from '../../__tests__/spawn-utils'; 6import { YarnPackageManager } from '../YarnPackageManager'; 7 8jest.mock('@expo/spawn-async'); 9jest.mock('fs'); 10 11beforeAll(() => { 12 // Disable logging to clean up test ouput 13 jest.spyOn(console, 'log').mockImplementation(); 14}); 15 16describe('YarnPackageManager', () => { 17 const projectRoot = '/project/with-yarn'; 18 19 it('name is set to yarn', () => { 20 const yarn = new YarnPackageManager({ cwd: projectRoot }); 21 expect(yarn.name).toBe('yarn'); 22 }); 23 24 describe('getDefaultEnvironment', () => { 25 it('runs npm with ADBLOCK=1 and DISABLE_OPENCOLLECTIVE=1', async () => { 26 const yarn = new YarnPackageManager({ cwd: projectRoot }); 27 await yarn.installAsync(); 28 29 expect(spawnAsync).toBeCalledWith( 30 expect.anything(), 31 expect.anything(), 32 expect.objectContaining({ 33 env: expect.objectContaining({ ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' }), 34 }) 35 ); 36 }); 37 38 it('runs with overwritten default environment', async () => { 39 const yarn = new YarnPackageManager({ cwd: projectRoot, env: { ADBLOCK: '0' } }); 40 await yarn.installAsync(); 41 42 expect(spawnAsync).toBeCalledWith( 43 expect.anything(), 44 expect.anything(), 45 expect.objectContaining({ 46 env: { ADBLOCK: '0', DISABLE_OPENCOLLECTIVE: '1' }, 47 }) 48 ); 49 }); 50 }); 51 52 describe('runAsync', () => { 53 it('logs executed command', async () => { 54 const log = jest.fn(); 55 const yarn = new YarnPackageManager({ cwd: projectRoot, log }); 56 await yarn.runAsync(['install', '--some-flag']); 57 expect(log).toHaveBeenCalledWith('> yarn install --some-flag'); 58 }); 59 60 it('inherits stdio output without silent', async () => { 61 const yarn = new YarnPackageManager({ cwd: projectRoot }); 62 await yarn.runAsync(['install']); 63 64 expect(spawnAsync).toBeCalledWith( 65 expect.anything(), 66 expect.anything(), 67 expect.objectContaining({ stdio: 'inherit' }) 68 ); 69 }); 70 71 it('does not inherit stdio with silent', async () => { 72 const yarn = new YarnPackageManager({ cwd: projectRoot, silent: true }); 73 await yarn.runAsync(['install']); 74 75 expect(spawnAsync).toBeCalledWith( 76 expect.anything(), 77 expect.anything(), 78 expect.objectContaining({ stdio: undefined }) 79 ); 80 }); 81 82 it('adds a single package with custom parameters', async () => { 83 const yarn = new YarnPackageManager({ cwd: projectRoot }); 84 await yarn.runAsync(['add', '--peer', '@babel/core']); 85 86 expect(spawnAsync).toBeCalledWith( 87 'yarnpkg', 88 ['add', '--peer', '@babel/core'], 89 expect.objectContaining({ cwd: projectRoot }) 90 ); 91 }); 92 93 it('adds multiple packages with custom parameters', async () => { 94 const yarn = new YarnPackageManager({ cwd: projectRoot }); 95 await yarn.runAsync(['add', '--peer', '@babel/core', '@babel/runtime']); 96 97 expect(spawnAsync).toBeCalledWith( 98 'yarnpkg', 99 ['add', '--peer', '@babel/core', '@babel/runtime'], 100 expect.objectContaining({ cwd: projectRoot }) 101 ); 102 }); 103 }); 104 105 describe('versionAsync', () => { 106 it('returns version from yarn', async () => { 107 mockedSpawnAsync.mockImplementation(() => 108 mockSpawnPromise(Promise.resolve({ stdout: '4.2.0\n' })) 109 ); 110 111 const yarn = new YarnPackageManager({ cwd: projectRoot }); 112 113 expect(await yarn.versionAsync()).toBe('4.2.0'); 114 expect(spawnAsync).toBeCalledWith('yarnpkg', ['--version'], expect.anything()); 115 }); 116 }); 117 118 describe('getConfigAsync', () => { 119 it('returns a configuration key from yarn', async () => { 120 mockedSpawnAsync.mockImplementation(() => 121 mockSpawnPromise(Promise.resolve({ stdout: 'https://custom.registry.org/\n' })) 122 ); 123 124 const yarn = new YarnPackageManager({ cwd: projectRoot }); 125 126 expect(await yarn.getConfigAsync('registry')).toBe('https://custom.registry.org/'); 127 expect(spawnAsync).toBeCalledWith( 128 'yarnpkg', 129 ['config', 'get', 'registry'], 130 expect.anything() 131 ); 132 }); 133 }); 134 135 describe('installAsync', () => { 136 it('runs normal installation', async () => { 137 const yarn = new YarnPackageManager({ cwd: projectRoot }); 138 await yarn.installAsync(); 139 140 expect(spawnAsync).toBeCalledWith( 141 'yarnpkg', 142 ['install'], 143 expect.objectContaining({ cwd: projectRoot }) 144 ); 145 }); 146 147 it('runs installation with flags', async () => { 148 const yarn = new YarnPackageManager({ cwd: projectRoot }); 149 await yarn.installAsync(['--ignore-scripts']); 150 151 expect(spawnAsync).toBeCalledWith( 152 'yarnpkg', 153 ['install', '--ignore-scripts'], 154 expect.objectContaining({ cwd: projectRoot }) 155 ); 156 }); 157 }); 158 159 describe('uninstallAsync', () => { 160 afterEach(() => vol.reset()); 161 162 it('removes node_modules folder relative to cwd', async () => { 163 vol.fromJSON( 164 { 165 'package.json': '{}', 166 'node_modules/expo/package.json': '{}', 167 }, 168 projectRoot 169 ); 170 171 const yarn = new YarnPackageManager({ cwd: projectRoot }); 172 await yarn.uninstallAsync(); 173 174 expect(vol.existsSync(path.join(projectRoot, 'node_modules'))).toBe(false); 175 }); 176 177 it('skips removing non-existing node_modules folder', async () => { 178 vol.fromJSON({ 'package.json': '{}' }, projectRoot); 179 180 const yarn = new YarnPackageManager({ cwd: projectRoot }); 181 await yarn.uninstallAsync(); 182 183 expect(vol.existsSync(path.join(projectRoot, 'node_modules'))).toBe(false); 184 }); 185 186 it('fails when no cwd is provided', async () => { 187 const yarn = new YarnPackageManager({ cwd: undefined }); 188 await expect(yarn.uninstallAsync()).rejects.toThrow('cwd is required'); 189 }); 190 }); 191 192 describe('addAsync', () => { 193 it('installs project without packages', async () => { 194 const yarn = new YarnPackageManager({ cwd: projectRoot }); 195 await yarn.addAsync(); 196 197 expect(spawnAsync).toBeCalledWith( 198 'yarnpkg', 199 ['install'], 200 expect.objectContaining({ cwd: projectRoot }) 201 ); 202 }); 203 204 it('returns pending spawn promise with child', async () => { 205 const yarn = new YarnPackageManager({ cwd: projectRoot }); 206 const pending = yarn.addAsync(['expo']); 207 208 expect(pending).toHaveProperty('child', expect.any(Promise)); 209 await expect(pending.child).resolves.toMatchObject(STUB_SPAWN_CHILD); 210 }); 211 212 it('adds a single package to dependencies', async () => { 213 const yarn = new YarnPackageManager({ cwd: projectRoot }); 214 await yarn.addAsync(['@react-navigation/native']); 215 216 expect(spawnAsync).toBeCalledWith( 217 'yarnpkg', 218 ['add', '@react-navigation/native'], 219 expect.objectContaining({ cwd: projectRoot }) 220 ); 221 }); 222 223 it('adds multiple packages to dependencies', async () => { 224 const yarn = new YarnPackageManager({ cwd: projectRoot }); 225 await yarn.addAsync(['@react-navigation/native', '@react-navigation/drawer']); 226 227 expect(spawnAsync).toBeCalledWith( 228 'yarnpkg', 229 ['add', '@react-navigation/native', '@react-navigation/drawer'], 230 expect.objectContaining({ cwd: projectRoot }) 231 ); 232 }); 233 }); 234 235 describe('addDevAsync', () => { 236 it('installs project without packages', async () => { 237 const yarn = new YarnPackageManager({ cwd: projectRoot }); 238 await yarn.addDevAsync(); 239 240 expect(spawnAsync).toBeCalledWith( 241 'yarnpkg', 242 ['install'], 243 expect.objectContaining({ cwd: projectRoot }) 244 ); 245 }); 246 247 it('returns pending spawn promise with child', async () => { 248 const yarn = new YarnPackageManager({ cwd: projectRoot }); 249 const pending = yarn.addDevAsync(['expo']); 250 251 expect(pending).toHaveProperty('child', expect.any(Promise)); 252 await expect(pending.child).resolves.toMatchObject(STUB_SPAWN_CHILD); 253 }); 254 255 it('adds a single package to dev dependencies', async () => { 256 const yarn = new YarnPackageManager({ cwd: projectRoot }); 257 await yarn.addDevAsync(['eslint']); 258 259 expect(spawnAsync).toBeCalledWith( 260 'yarnpkg', 261 ['add', '--dev', 'eslint'], 262 expect.objectContaining({ cwd: projectRoot }) 263 ); 264 }); 265 266 it('adds multiple packages to dev dependencies', async () => { 267 const yarn = new YarnPackageManager({ cwd: projectRoot }); 268 await yarn.addDevAsync(['eslint', 'prettier']); 269 270 expect(spawnAsync).toBeCalledWith( 271 'yarnpkg', 272 ['add', '--dev', 'eslint', 'prettier'], 273 expect.objectContaining({ cwd: projectRoot }) 274 ); 275 }); 276 }); 277 278 describe('addGlobalAsync', () => { 279 it('installs project without packages', async () => { 280 const yarn = new YarnPackageManager({ cwd: projectRoot }); 281 await yarn.addGlobalAsync(); 282 283 expect(spawnAsync).toBeCalledWith( 284 'yarnpkg', 285 ['install'], 286 expect.objectContaining({ cwd: projectRoot }) 287 ); 288 }); 289 290 it('returns pending spawn promise with child', async () => { 291 const yarn = new YarnPackageManager({ cwd: projectRoot }); 292 const pending = yarn.addGlobalAsync(['expo']); 293 294 expect(pending).toHaveProperty('child', expect.any(Promise)); 295 await expect(pending.child).resolves.toMatchObject(STUB_SPAWN_CHILD); 296 }); 297 298 it('adds a single package globally', async () => { 299 const yarn = new YarnPackageManager({ cwd: projectRoot }); 300 await yarn.addGlobalAsync(['expo-cli@^5']); 301 302 expect(spawnAsync).toBeCalledWith( 303 'yarnpkg', 304 ['global', 'add', 'expo-cli@^5'], 305 expect.anything() 306 ); 307 }); 308 309 it('adds multiple packages globally', async () => { 310 const yarn = new YarnPackageManager({ cwd: projectRoot }); 311 await yarn.addGlobalAsync(['expo-cli@^5', 'eas-cli']); 312 313 expect(spawnAsync).toBeCalledWith( 314 'yarnpkg', 315 ['global', 'add', 'expo-cli@^5', 'eas-cli'], 316 expect.anything() 317 ); 318 }); 319 }); 320 321 describe('removeAsync', () => { 322 it('removes a single package', async () => { 323 const yarn = new YarnPackageManager({ cwd: projectRoot }); 324 await yarn.removeAsync(['metro']); 325 326 expect(spawnAsync).toBeCalledWith( 327 'yarnpkg', 328 ['remove', 'metro'], 329 expect.objectContaining({ cwd: projectRoot }) 330 ); 331 }); 332 333 it('removes multiple packages', async () => { 334 const yarn = new YarnPackageManager({ cwd: projectRoot }); 335 await yarn.removeAsync(['metro', 'jest-haste-map']); 336 337 expect(spawnAsync).toBeCalledWith( 338 'yarnpkg', 339 ['remove', 'metro', 'jest-haste-map'], 340 expect.objectContaining({ cwd: projectRoot }) 341 ); 342 }); 343 }); 344 345 describe('removeDevAsync', () => { 346 it('removes a single package', async () => { 347 const yarn = new YarnPackageManager({ cwd: projectRoot }); 348 await yarn.removeDevAsync(['metro']); 349 350 expect(spawnAsync).toBeCalledWith( 351 'yarnpkg', 352 ['remove', 'metro'], 353 expect.objectContaining({ cwd: projectRoot }) 354 ); 355 }); 356 357 it('removes multiple packages', async () => { 358 const yarn = new YarnPackageManager({ cwd: projectRoot }); 359 await yarn.removeDevAsync(['metro', 'jest-haste-map']); 360 361 expect(spawnAsync).toBeCalledWith( 362 'yarnpkg', 363 ['remove', 'metro', 'jest-haste-map'], 364 expect.objectContaining({ cwd: projectRoot }) 365 ); 366 }); 367 }); 368 369 describe('removeGlobalAsync', () => { 370 it('removes a single package', async () => { 371 const yarn = new YarnPackageManager({ cwd: projectRoot }); 372 await yarn.removeGlobalAsync(['expo-cli']); 373 374 expect(spawnAsync).toBeCalledWith( 375 'yarnpkg', 376 ['global', 'remove', 'expo-cli'], 377 expect.objectContaining({ cwd: projectRoot }) 378 ); 379 }); 380 381 it('removes multiple packages', async () => { 382 const yarn = new YarnPackageManager({ cwd: projectRoot }); 383 await yarn.removeGlobalAsync(['expo-cli', 'eas-cli']); 384 385 expect(spawnAsync).toBeCalledWith( 386 'yarnpkg', 387 ['global', 'remove', 'expo-cli', 'eas-cli'], 388 expect.objectContaining({ cwd: projectRoot }) 389 ); 390 }); 391 }); 392 393 describe('workspaceRoot', () => { 394 const workspaceRoot = '/monorepo'; 395 const projectRoot = '/monorepo/packages/test'; 396 397 it('returns null for non-monorepo project', () => { 398 vol.fromJSON( 399 { 400 'package.json': JSON.stringify({ name: 'project' }), 401 }, 402 projectRoot 403 ); 404 405 const yarn = new YarnPackageManager({ cwd: projectRoot }); 406 expect(yarn.workspaceRoot()).toBeNull(); 407 }); 408 409 it('returns new instance for monorepo project', () => { 410 vol.fromJSON( 411 { 412 'packages/test/package.json': JSON.stringify({ name: 'project' }), 413 'package.json': JSON.stringify({ 414 name: 'monorepo', 415 workspaces: ['packages/*'], 416 }), 417 }, 418 workspaceRoot 419 ); 420 421 const yarn = new YarnPackageManager({ cwd: projectRoot }); 422 const root = yarn.workspaceRoot(); 423 expect(root).toBeInstanceOf(YarnPackageManager); 424 expect(root).not.toBe(yarn); 425 }); 426 }); 427 428 // describe('offline support', () => { 429 // // TODO 430 // }); 431}); 432