1/* eslint-env jest */
2import JsonFile from '@expo/json-file';
3import execa, { ExecaError } from 'execa';
4import fs from 'fs/promises';
5import klawSync from 'klaw-sync';
6import path from 'path';
7
8import {
9  execute,
10  projectRoot,
11  getLoadedModulesAsync,
12  bin,
13  setupTestProjectAsync,
14  installAsync,
15} from './utils';
16
17const originalForceColor = process.env.FORCE_COLOR;
18const originalCI = process.env.CI;
19beforeAll(async () => {
20  await fs.mkdir(projectRoot, { recursive: true });
21  process.env.FORCE_COLOR = '0';
22  process.env.CI = '1';
23});
24afterAll(() => {
25  process.env.FORCE_COLOR = originalForceColor;
26  process.env.CI = originalCI;
27});
28
29it('loads expected modules by default', async () => {
30  const modules = await getLoadedModulesAsync(`require('../../build/src/install').expoInstall`);
31  expect(modules).toStrictEqual([
32    '../node_modules/arg/index.js',
33    '../node_modules/chalk/node_modules/ansi-styles/index.js',
34    '../node_modules/chalk/source/index.js',
35    '../node_modules/chalk/source/util.js',
36    '../node_modules/has-flag/index.js',
37    '../node_modules/supports-color/index.js',
38    '@expo/cli/build/src/install/index.js',
39    '@expo/cli/build/src/log.js',
40    '@expo/cli/build/src/utils/args.js',
41  ]);
42});
43
44it('runs `npx install install --help`', async () => {
45  const results = await execute('install', '--help');
46  expect(results.stdout).toMatchInlineSnapshot(`
47    "
48      Info
49        Install a module or other package to a project
50
51      Usage
52        $ npx expo install
53
54      Options
55        --check     Check which installed packages need to be updated
56        --fix       Automatically update any invalid package versions
57        --npm       Use npm to install dependencies. Default when package-lock.json exists
58        --yarn      Use Yarn to install dependencies. Default when yarn.lock exists
59        --pnpm      Use pnpm to install dependencies. Default when pnpm-lock.yaml exists
60        -h, --help  Usage info
61
62      Additional options can be passed to the underlying install command by using --
63        $ npx expo install react -- --verbose
64        > yarn add react --verbose
65    "
66  `);
67});
68
69it(
70  'runs `npx expo install expo-sms`',
71  async () => {
72    const projectRoot = await setupTestProjectAsync('basic-install', 'with-blank');
73    // `npx expo install expo-sms`
74    await execa('node', [bin, 'install', 'expo-sms'], { cwd: projectRoot });
75
76    // List output files with sizes for snapshotting.
77    // This is to make sure that any changes to the output are intentional.
78    // Posix path formatting is used to make paths the same across OSes.
79    const files = klawSync(projectRoot)
80      .map((entry) => {
81        if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
82          return null;
83        }
84        return path.posix.relative(projectRoot, entry.path);
85      })
86      .filter(Boolean);
87
88    const pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
89
90    // Added expected package
91    const pkgDependencies = pkg.dependencies as Record<string, string>;
92    expect(pkgDependencies['expo-sms']).toBe('~10.2.0');
93    expect(pkg.devDependencies).toEqual({
94      '@babel/core': '^7.12.9',
95    });
96
97    // Added new packages
98    expect(Object.keys(pkg.dependencies ?? {}).sort()).toStrictEqual([
99      'expo',
100      'expo-sms',
101      'react',
102      'react-native',
103    ]);
104
105    expect(files).toStrictEqual(['App.js', 'app.json', 'package.json', 'yarn.lock']);
106  },
107  // Could take 45s depending on how fast npm installs
108  60 * 1000
109);
110
111it(
112  'runs `npx expo install --check` fails',
113  async () => {
114    const projectRoot = await setupTestProjectAsync('install-check-fail', 'with-blank');
115    await installAsync(projectRoot, ['add', '[email protected]', '[email protected]']);
116
117    let pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
118    // Added expected package
119    let pkgDependencies = pkg.dependencies as Record<string, string>;
120    expect(pkgDependencies['expo-sms']).toBe('1.0.0');
121
122    try {
123      await execa('node', [bin, 'install', '--check'], { cwd: projectRoot });
124      throw new Error('SHOULD NOT HAPPEN');
125    } catch (e) {
126      const error = e as ExecaError;
127      expect(error.stderr).toMatch(/expo-auth-session@1\.0\.0 - expected version: ~3\.\d\.\d/);
128      expect(error.stderr).toMatch(/expo-sms@1\.0\.0 - expected version: ~10\.\d\.\d/);
129      expect(error.stderr).toMatch(
130        /npx expo install expo-auth-session@~3\.\d\.\d expo-sms@~10\.\d\.\d/
131      );
132    }
133
134    await expect(
135      execa('node', [bin, 'install', 'expo-sms', '--check'], { cwd: projectRoot })
136    ).rejects.toThrowError(/expo-sms@1\.0\.0 - expected version: ~10\.\d\.\d/);
137
138    // Check doesn't fix packages
139    pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
140    // Added expected package
141    pkgDependencies = pkg.dependencies as Record<string, string>;
142    expect(pkgDependencies['expo-sms']).toBe('1.0.0');
143  },
144  // Could take 45s depending on how fast npm installs
145  60 * 1000
146);
147
148it(
149  'runs `npx expo install --fix` fails',
150  async () => {
151    const projectRoot = await setupTestProjectAsync('install-fix-fail', 'with-blank');
152    await installAsync(projectRoot, ['add', '[email protected]', '[email protected]']);
153
154    await execa('node', [bin, 'install', '--fix', 'expo-sms'], { cwd: projectRoot });
155
156    // Ensure the versions are invalid
157    await expect(
158      execa('node', [bin, 'install', '--check'], { cwd: projectRoot })
159    ).rejects.toThrow();
160
161    // Check doesn't fix packages
162    let pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
163    // Added expected package
164    let pkgDependencies = pkg.dependencies as Record<string, string>;
165    expect(pkgDependencies['expo-sms']).toBe('~10.2.0');
166
167    // Didn't fix expo-auth-session since we didn't pass it in
168    expect(pkgDependencies['expo-auth-session']).toBe('1.0.0');
169
170    // Fix all versions
171    await execa('node', [bin, 'install', '--fix'], { cwd: projectRoot });
172
173    // Check that the versions are fixed
174    pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
175
176    // Didn't fix expo-auth-session since we didn't pass it in
177    pkgDependencies = pkg.dependencies as Record<string, string>;
178    expect(pkgDependencies['expo-auth-session']).toBe('~3.6.1');
179  },
180  // Could take 45s depending on how fast npm installs
181  60 * 1000
182);
183