1/* eslint-env jest */
2import JsonFile from '@expo/json-file';
3import execa from 'execa';
4import fs from 'fs/promises';
5import { sync as globSync } from 'glob';
6import klawSync from 'klaw-sync';
7import path from 'path';
8
9import {
10  bin,
11  execute,
12  projectRoot,
13  getRoot,
14  setupTestProjectAsync,
15  getLoadedModulesAsync,
16} from './utils';
17
18const originalForceColor = process.env.FORCE_COLOR;
19const originalCI = process.env.CI;
20
21const templateFolder = path.join(__dirname, '../../../../../templates/expo-template-bare-minimum/');
22
23function getTemplatePath() {
24  const results = globSync(`*.tgz`, {
25    absolute: true,
26    cwd: templateFolder,
27  });
28
29  return results[0];
30}
31
32async function ensureTemplatePathAsync() {
33  let templatePath = getTemplatePath();
34  if (templatePath) return templatePath;
35  await execa('npm', ['pack'], { cwd: templateFolder });
36
37  templatePath = getTemplatePath();
38  if (templatePath) return templatePath;
39
40  throw new Error('Could not find template tarball');
41}
42
43beforeAll(async () => {
44  await fs.mkdir(projectRoot, { recursive: true });
45  process.env.FORCE_COLOR = '0';
46  process.env.CI = '1';
47});
48
49afterAll(() => {
50  process.env.FORCE_COLOR = originalForceColor;
51  process.env.CI = originalCI;
52});
53
54it('loads expected modules by default', async () => {
55  const modules = await getLoadedModulesAsync(`require('../../build/src/prebuild').expoPrebuild`);
56  expect(modules).toStrictEqual([
57    '../node_modules/ansi-styles/index.js',
58    '../node_modules/arg/index.js',
59    '../node_modules/chalk/source/index.js',
60    '../node_modules/chalk/source/util.js',
61    '../node_modules/has-flag/index.js',
62    '../node_modules/supports-color/index.js',
63    '@expo/cli/build/src/log.js',
64    '@expo/cli/build/src/prebuild/index.js',
65    '@expo/cli/build/src/utils/args.js',
66  ]);
67});
68
69it('runs `npx expo prebuild --help`', async () => {
70  const results = await execute('prebuild', '--help');
71  expect(results.stdout).toMatchInlineSnapshot(`
72    "
73      Info
74        Create native iOS and Android project files for building natively
75
76      Usage
77        $ npx expo prebuild <dir>
78
79      Options
80        <dir>                                    Directory of the Expo project. Default: Current working directory
81        --no-install                             Skip installing npm packages and CocoaPods
82        --clean                                  Delete the native folders and regenerate them before applying changes
83        --npm                                    Use npm to install dependencies. Default when package-lock.json exists
84        --yarn                                   Use Yarn to install dependencies. Default when yarn.lock exists
85        --pnpm                                   Use pnpm to install dependencies. Default when pnpm-lock.yaml exists
86        --template <template>                    Project template to clone from. File path pointing to a local tar file or a github repo
87        -p, --platform <all|android|ios>         Platforms to sync: ios, android, all. Default: all
88        --skip-dependency-update <dependencies>  Preserves versions of listed packages in package.json (comma separated list)
89        -h, --help                               Usage info
90    "
91  `);
92});
93
94it('runs `npx expo prebuild` asserts when expo is not installed', async () => {
95  const projectName = 'basic-prebuild-assert-no-expo';
96  const projectRoot = getRoot(projectName);
97  // Create the project root aot
98  await fs.mkdir(projectRoot, { recursive: true });
99  // Create a fake package.json -- this is a terminal file that cannot be overwritten.
100  await fs.writeFile(path.join(projectRoot, 'package.json'), '{ "version": "1.0.0" }');
101  await fs.writeFile(path.join(projectRoot, 'app.json'), '{ "expo": { "name": "foobar" } }');
102
103  await expect(execute('prebuild', projectName, '--no-install')).rejects.toThrowError(
104    /Cannot determine which native SDK version your project uses because the module `expo` is not installed\. Please install it with `yarn add expo` and try again./
105  );
106});
107
108it(
109  'runs `npx expo prebuild`',
110  async () => {
111    const projectRoot = await setupTestProjectAsync('basic-prebuild', 'with-blank');
112    // `npx expo prebuild --no-install`
113
114    const templateFolder = await ensureTemplatePathAsync();
115    console.log('Using local template:', templateFolder);
116
117    await execa('node', [bin, 'prebuild', '--no-install', '--template', templateFolder], {
118      cwd: projectRoot,
119    });
120
121    // List output files with sizes for snapshotting.
122    // This is to make sure that any changes to the output are intentional.
123    // Posix path formatting is used to make paths the same across OSes.
124    const files = klawSync(projectRoot)
125      .map((entry) => {
126        if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
127          return null;
128        }
129        return path.posix.relative(projectRoot, entry.path);
130      })
131      .filter(Boolean);
132
133    const pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
134
135    // Added new packages
136    expect(Object.keys(pkg.dependencies ?? {}).sort()).toStrictEqual([
137      'expo',
138      'expo-splash-screen',
139      'expo-status-bar',
140      'react',
141      'react-native',
142    ]);
143
144    // Updated scripts
145    expect(pkg.scripts).toStrictEqual({
146      android: 'expo run:android',
147      ios: 'expo run:ios',
148    });
149
150    // If this changes then everything else probably changed as well.
151    expect(files).toMatchInlineSnapshot(`
152      [
153        "App.js",
154        "android/.gitignore",
155        "android/app/build.gradle",
156        "android/app/debug.keystore",
157        "android/app/proguard-rules.pro",
158        "android/app/src/debug/AndroidManifest.xml",
159        "android/app/src/debug/java/com/example/minimal/ReactNativeFlipper.java",
160        "android/app/src/main/AndroidManifest.xml",
161        "android/app/src/main/java/com/example/minimal/MainActivity.java",
162        "android/app/src/main/java/com/example/minimal/MainApplication.java",
163        "android/app/src/main/res/drawable/rn_edit_text_material.xml",
164        "android/app/src/main/res/drawable/splashscreen.xml",
165        "android/app/src/main/res/mipmap-hdpi/ic_launcher.png",
166        "android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png",
167        "android/app/src/main/res/mipmap-mdpi/ic_launcher.png",
168        "android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png",
169        "android/app/src/main/res/mipmap-xhdpi/ic_launcher.png",
170        "android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png",
171        "android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png",
172        "android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png",
173        "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png",
174        "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png",
175        "android/app/src/main/res/values/colors.xml",
176        "android/app/src/main/res/values/strings.xml",
177        "android/app/src/main/res/values/styles.xml",
178        "android/app/src/main/res/values-night/colors.xml",
179        "android/app/src/release/java/com/example/minimal/ReactNativeFlipper.java",
180        "android/build.gradle",
181        "android/gradle/wrapper/gradle-wrapper.jar",
182        "android/gradle/wrapper/gradle-wrapper.properties",
183        "android/gradle.properties",
184        "android/gradlew",
185        "android/gradlew.bat",
186        "android/settings.gradle",
187        "app.json",
188        "ios/.gitignore",
189        "ios/.xcode.env",
190        "ios/Podfile",
191        "ios/Podfile.properties.json",
192        "ios/basicprebuild/AppDelegate.h",
193        "ios/basicprebuild/AppDelegate.mm",
194        "ios/basicprebuild/Images.xcassets/AppIcon.appiconset/Contents.json",
195        "ios/basicprebuild/Images.xcassets/Contents.json",
196        "ios/basicprebuild/Images.xcassets/SplashScreenBackground.imageset/Contents.json",
197        "ios/basicprebuild/Images.xcassets/SplashScreenBackground.imageset/image.png",
198        "ios/basicprebuild/Info.plist",
199        "ios/basicprebuild/SplashScreen.storyboard",
200        "ios/basicprebuild/Supporting/Expo.plist",
201        "ios/basicprebuild/basicprebuild-Bridging-Header.h",
202        "ios/basicprebuild/basicprebuild.entitlements",
203        "ios/basicprebuild/main.m",
204        "ios/basicprebuild/noop-file.swift",
205        "ios/basicprebuild.xcodeproj/project.pbxproj",
206        "ios/basicprebuild.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
207        "ios/basicprebuild.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
208        "ios/basicprebuild.xcodeproj/xcshareddata/xcschemes/basicprebuild.xcscheme",
209        "package.json",
210        "yarn.lock",
211      ]
212    `);
213  },
214  // Could take 45s depending on how fast npm installs
215  60 * 1000
216);
217