109bb6093SEvan Bacon/* eslint-env jest */
209bb6093SEvan Baconimport JsonFile from '@expo/json-file';
3e1bb5bdfSKudo Chienimport execa, { ExecaError } from 'execa';
409bb6093SEvan Baconimport fs from 'fs/promises';
509bb6093SEvan Baconimport klawSync from 'klaw-sync';
609bb6093SEvan Baconimport path from 'path';
709bb6093SEvan Bacon
809bb6093SEvan Baconimport {
909bb6093SEvan Bacon  execute,
1009bb6093SEvan Bacon  projectRoot,
1109bb6093SEvan Bacon  getLoadedModulesAsync,
1209bb6093SEvan Bacon  bin,
1309bb6093SEvan Bacon  setupTestProjectAsync,
14c94ad8a2SEvan Bacon  installAsync,
1509bb6093SEvan Bacon} from './utils';
1609bb6093SEvan Bacon
1709bb6093SEvan Baconconst originalForceColor = process.env.FORCE_COLOR;
18c94ad8a2SEvan Baconconst originalCI = process.env.CI;
1909bb6093SEvan BaconbeforeAll(async () => {
2009bb6093SEvan Bacon  await fs.mkdir(projectRoot, { recursive: true });
2109bb6093SEvan Bacon  process.env.FORCE_COLOR = '0';
22c94ad8a2SEvan Bacon  process.env.CI = '1';
2309bb6093SEvan Bacon});
2409bb6093SEvan BaconafterAll(() => {
2509bb6093SEvan Bacon  process.env.FORCE_COLOR = originalForceColor;
26c94ad8a2SEvan Bacon  process.env.CI = originalCI;
2709bb6093SEvan Bacon});
2809bb6093SEvan Bacon
2909bb6093SEvan Baconit('loads expected modules by default', async () => {
3009bb6093SEvan Bacon  const modules = await getLoadedModulesAsync(`require('../../build/src/install').expoInstall`);
3109bb6093SEvan Bacon  expect(modules).toStrictEqual([
324067174dSWill Schurman    '../node_modules/ansi-styles/index.js',
3309bb6093SEvan Bacon    '../node_modules/arg/index.js',
3409bb6093SEvan Bacon    '../node_modules/chalk/source/index.js',
3509bb6093SEvan Bacon    '../node_modules/chalk/source/util.js',
3609bb6093SEvan Bacon    '../node_modules/has-flag/index.js',
3709bb6093SEvan Bacon    '../node_modules/supports-color/index.js',
3809bb6093SEvan Bacon    '@expo/cli/build/src/install/index.js',
3909bb6093SEvan Bacon    '@expo/cli/build/src/log.js',
4009bb6093SEvan Bacon    '@expo/cli/build/src/utils/args.js',
4109bb6093SEvan Bacon  ]);
4209bb6093SEvan Bacon});
4309bb6093SEvan Bacon
4409bb6093SEvan Baconit('runs `npx install install --help`', async () => {
4509bb6093SEvan Bacon  const results = await execute('install', '--help');
4609bb6093SEvan Bacon  expect(results.stdout).toMatchInlineSnapshot(`
4709bb6093SEvan Bacon    "
4883d464dcSEvan Bacon      Info
4909bb6093SEvan Bacon        Install a module or other package to a project
5009bb6093SEvan Bacon
5109bb6093SEvan Bacon      Usage
5283d464dcSEvan Bacon        $ npx expo install
5309bb6093SEvan Bacon
5409bb6093SEvan Bacon      Options
5583d464dcSEvan Bacon        --check     Check which installed packages need to be updated
5683d464dcSEvan Bacon        --fix       Automatically update any invalid package versions
5709bb6093SEvan Bacon        --npm       Use npm to install dependencies. Default when package-lock.json exists
5809bb6093SEvan Bacon        --yarn      Use Yarn to install dependencies. Default when yarn.lock exists
59*9b1b5ec6SEvan Bacon        --bun       Use bun to install dependencies. Default when bun.lockb exists
606caf5755SEvan Bacon        --pnpm      Use pnpm to install dependencies. Default when pnpm-lock.yaml exists
6183d464dcSEvan Bacon        -h, --help  Usage info
6209bb6093SEvan Bacon
6309bb6093SEvan Bacon      Additional options can be passed to the underlying install command by using --
6483d464dcSEvan Bacon        $ npx expo install react -- --verbose
6509bb6093SEvan Bacon        > yarn add react --verbose
6609bb6093SEvan Bacon    "
6709bb6093SEvan Bacon  `);
6809bb6093SEvan Bacon});
6909bb6093SEvan Bacon
7009bb6093SEvan Baconit(
7109bb6093SEvan Bacon  'runs `npx expo install expo-sms`',
7209bb6093SEvan Bacon  async () => {
7309bb6093SEvan Bacon    const projectRoot = await setupTestProjectAsync('basic-install', 'with-blank');
7409bb6093SEvan Bacon    // `npx expo install expo-sms`
7509bb6093SEvan Bacon    await execa('node', [bin, 'install', 'expo-sms'], { cwd: projectRoot });
7609bb6093SEvan Bacon
7709bb6093SEvan Bacon    // List output files with sizes for snapshotting.
7809bb6093SEvan Bacon    // This is to make sure that any changes to the output are intentional.
7909bb6093SEvan Bacon    // Posix path formatting is used to make paths the same across OSes.
8009bb6093SEvan Bacon    const files = klawSync(projectRoot)
8109bb6093SEvan Bacon      .map((entry) => {
8209bb6093SEvan Bacon        if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
8309bb6093SEvan Bacon          return null;
8409bb6093SEvan Bacon        }
8509bb6093SEvan Bacon        return path.posix.relative(projectRoot, entry.path);
8609bb6093SEvan Bacon      })
8709bb6093SEvan Bacon      .filter(Boolean);
8809bb6093SEvan Bacon
8909bb6093SEvan Bacon    const pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
9009bb6093SEvan Bacon
9109bb6093SEvan Bacon    // Added expected package
92e1bb5bdfSKudo Chien    const pkgDependencies = pkg.dependencies as Record<string, string>;
93425e7f7fSEvan Bacon    expect(pkgDependencies['expo-sms']).toBe('~11.0.0');
9409bb6093SEvan Bacon    expect(pkg.devDependencies).toEqual({
9509bb6093SEvan Bacon      '@babel/core': '^7.12.9',
9609bb6093SEvan Bacon    });
9709bb6093SEvan Bacon
9809bb6093SEvan Bacon    // Added new packages
99e1bb5bdfSKudo Chien    expect(Object.keys(pkg.dependencies ?? {}).sort()).toStrictEqual([
10009bb6093SEvan Bacon      'expo',
10109bb6093SEvan Bacon      'expo-sms',
10209bb6093SEvan Bacon      'react',
10309bb6093SEvan Bacon      'react-native',
10409bb6093SEvan Bacon    ]);
10509bb6093SEvan Bacon
10609bb6093SEvan Bacon    expect(files).toStrictEqual(['App.js', 'app.json', 'package.json', 'yarn.lock']);
10709bb6093SEvan Bacon  },
10809bb6093SEvan Bacon  // Could take 45s depending on how fast npm installs
10909bb6093SEvan Bacon  60 * 1000
11009bb6093SEvan Bacon);
111c94ad8a2SEvan Bacon
112c94ad8a2SEvan Baconit(
113c94ad8a2SEvan Bacon  'runs `npx expo install --check` fails',
114c94ad8a2SEvan Bacon  async () => {
115c94ad8a2SEvan Bacon    const projectRoot = await setupTestProjectAsync('install-check-fail', 'with-blank');
116c94ad8a2SEvan Bacon    await installAsync(projectRoot, ['add', '[email protected]', '[email protected]']);
117c94ad8a2SEvan Bacon
118c94ad8a2SEvan Bacon    let pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
119c94ad8a2SEvan Bacon    // Added expected package
120e1bb5bdfSKudo Chien    let pkgDependencies = pkg.dependencies as Record<string, string>;
121e1bb5bdfSKudo Chien    expect(pkgDependencies['expo-sms']).toBe('1.0.0');
122c94ad8a2SEvan Bacon
123c94ad8a2SEvan Bacon    try {
124c94ad8a2SEvan Bacon      await execa('node', [bin, 'install', '--check'], { cwd: projectRoot });
125c94ad8a2SEvan Bacon      throw new Error('SHOULD NOT HAPPEN');
126e1bb5bdfSKudo Chien    } catch (e) {
127e1bb5bdfSKudo Chien      const error = e as ExecaError;
128fa5d5955SEvan Bacon      expect(error.stderr).toMatch(/expo-auth-session@1\.0\.0 - expected version: ~3\.\d\.\d/);
129425e7f7fSEvan Bacon      expect(error.stderr).toMatch(/expo-sms@1\.0\.0 - expected version: ~11\.\d\.\d/);
130fb2f654cSEvan Bacon      expect(error.stderr).toMatch(/npx expo install --fix/);
131c94ad8a2SEvan Bacon    }
132c94ad8a2SEvan Bacon
133c94ad8a2SEvan Bacon    await expect(
134c94ad8a2SEvan Bacon      execa('node', [bin, 'install', 'expo-sms', '--check'], { cwd: projectRoot })
135425e7f7fSEvan Bacon    ).rejects.toThrowError(/expo-sms@1\.0\.0 - expected version: ~11\.\d\.\d/);
136c94ad8a2SEvan Bacon
137c94ad8a2SEvan Bacon    // Check doesn't fix packages
138c94ad8a2SEvan Bacon    pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
139c94ad8a2SEvan Bacon    // Added expected package
140e1bb5bdfSKudo Chien    pkgDependencies = pkg.dependencies as Record<string, string>;
141e1bb5bdfSKudo Chien    expect(pkgDependencies['expo-sms']).toBe('1.0.0');
142c94ad8a2SEvan Bacon  },
143c94ad8a2SEvan Bacon  // Could take 45s depending on how fast npm installs
144c94ad8a2SEvan Bacon  60 * 1000
145c94ad8a2SEvan Bacon);
146c94ad8a2SEvan Bacon
147c94ad8a2SEvan Baconit(
148c94ad8a2SEvan Bacon  'runs `npx expo install --fix` fails',
149c94ad8a2SEvan Bacon  async () => {
150c94ad8a2SEvan Bacon    const projectRoot = await setupTestProjectAsync('install-fix-fail', 'with-blank');
151c94ad8a2SEvan Bacon    await installAsync(projectRoot, ['add', '[email protected]', '[email protected]']);
152c94ad8a2SEvan Bacon
153c94ad8a2SEvan Bacon    await execa('node', [bin, 'install', '--fix', 'expo-sms'], { cwd: projectRoot });
154c94ad8a2SEvan Bacon
155c94ad8a2SEvan Bacon    // Ensure the versions are invalid
156c94ad8a2SEvan Bacon    await expect(
157c94ad8a2SEvan Bacon      execa('node', [bin, 'install', '--check'], { cwd: projectRoot })
158c94ad8a2SEvan Bacon    ).rejects.toThrow();
159c94ad8a2SEvan Bacon
160c94ad8a2SEvan Bacon    // Check doesn't fix packages
161c94ad8a2SEvan Bacon    let pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
162c94ad8a2SEvan Bacon    // Added expected package
163e1bb5bdfSKudo Chien    let pkgDependencies = pkg.dependencies as Record<string, string>;
164425e7f7fSEvan Bacon    expect(pkgDependencies['expo-sms']).toBe('~11.0.0');
165c94ad8a2SEvan Bacon
166c94ad8a2SEvan Bacon    // Didn't fix expo-auth-session since we didn't pass it in
167e1bb5bdfSKudo Chien    expect(pkgDependencies['expo-auth-session']).toBe('1.0.0');
168c94ad8a2SEvan Bacon
169c94ad8a2SEvan Bacon    // Fix all versions
170c94ad8a2SEvan Bacon    await execa('node', [bin, 'install', '--fix'], { cwd: projectRoot });
171c94ad8a2SEvan Bacon
172c94ad8a2SEvan Bacon    // Check that the versions are fixed
173c94ad8a2SEvan Bacon    pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
174c94ad8a2SEvan Bacon
175c94ad8a2SEvan Bacon    // Didn't fix expo-auth-session since we didn't pass it in
176e1bb5bdfSKudo Chien    pkgDependencies = pkg.dependencies as Record<string, string>;
177425e7f7fSEvan Bacon    expect(pkgDependencies['expo-auth-session']).toBe('~3.8.0');
178c94ad8a2SEvan Bacon  },
179c94ad8a2SEvan Bacon  // Could take 45s depending on how fast npm installs
180c94ad8a2SEvan Bacon  60 * 1000
181c94ad8a2SEvan Bacon);
182