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        --bun                                    Use bun to install dependencies. Default when bun.lockb exists
86        --pnpm                                   Use pnpm to install dependencies. Default when pnpm-lock.yaml exists
87        --template <template>                    Project template to clone from. File path pointing to a local tar file or a github repo
88        -p, --platform <all|android|ios>         Platforms to sync: ios, android, all. Default: all
89        --skip-dependency-update <dependencies>  Preserves versions of listed packages in package.json (comma separated list)
90        -h, --help                               Usage info
91    "
92  `);
93});
94
95it('runs `npx expo prebuild` asserts when expo is not installed', async () => {
96  const projectName = 'basic-prebuild-assert-no-expo';
97  const projectRoot = getRoot(projectName);
98  // Create the project root aot
99  await fs.mkdir(projectRoot, { recursive: true });
100  // Create a fake package.json -- this is a terminal file that cannot be overwritten.
101  await fs.writeFile(path.join(projectRoot, 'package.json'), '{ "version": "1.0.0" }');
102  await fs.writeFile(path.join(projectRoot, 'app.json'), '{ "expo": { "name": "foobar" } }');
103
104  await expect(execute('prebuild', projectName, '--no-install')).rejects.toThrowError(
105    /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./
106  );
107});
108
109it(
110  'runs `npx expo prebuild`',
111  async () => {
112    const projectRoot = await setupTestProjectAsync('basic-prebuild', 'with-blank');
113    // `npx expo prebuild --no-install`
114
115    const templateFolder = await ensureTemplatePathAsync();
116    console.log('Using local template:', templateFolder);
117
118    await execa('node', [bin, 'prebuild', '--no-install', '--template', templateFolder], {
119      cwd: projectRoot,
120    });
121
122    // List output files with sizes for snapshotting.
123    // This is to make sure that any changes to the output are intentional.
124    // Posix path formatting is used to make paths the same across OSes.
125    const files = klawSync(projectRoot)
126      .map((entry) => {
127        if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
128          return null;
129        }
130        return path.posix.relative(projectRoot, entry.path);
131      })
132      .filter(Boolean);
133
134    const pkg = await JsonFile.readAsync(path.resolve(projectRoot, 'package.json'));
135
136    // Added new packages
137    expect(Object.keys(pkg.dependencies ?? {}).sort()).toStrictEqual([
138      'expo',
139      'expo-splash-screen',
140      'expo-status-bar',
141      'react',
142      'react-native',
143    ]);
144
145    // Updated scripts
146    expect(pkg.scripts).toStrictEqual({
147      android: 'expo run:android',
148      ios: 'expo run:ios',
149    });
150
151    // If this changes then everything else probably changed as well.
152    expect(files).toMatchInlineSnapshot(`
153      [
154        "App.js",
155        "android/.gitignore",
156        "android/app/build.gradle",
157        "android/app/debug.keystore",
158        "android/app/proguard-rules.pro",
159        "android/app/src/debug/AndroidManifest.xml",
160        "android/app/src/debug/java/com/example/minimal/ReactNativeFlipper.java",
161        "android/app/src/main/AndroidManifest.xml",
162        "android/app/src/main/java/com/example/minimal/MainActivity.java",
163        "android/app/src/main/java/com/example/minimal/MainApplication.java",
164        "android/app/src/main/res/drawable/rn_edit_text_material.xml",
165        "android/app/src/main/res/drawable/splashscreen.xml",
166        "android/app/src/main/res/mipmap-hdpi/ic_launcher.png",
167        "android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png",
168        "android/app/src/main/res/mipmap-mdpi/ic_launcher.png",
169        "android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png",
170        "android/app/src/main/res/mipmap-xhdpi/ic_launcher.png",
171        "android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png",
172        "android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png",
173        "android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png",
174        "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png",
175        "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png",
176        "android/app/src/main/res/values/colors.xml",
177        "android/app/src/main/res/values/strings.xml",
178        "android/app/src/main/res/values/styles.xml",
179        "android/app/src/main/res/values-night/colors.xml",
180        "android/app/src/release/java/com/example/minimal/ReactNativeFlipper.java",
181        "android/build.gradle",
182        "android/gradle/wrapper/gradle-wrapper.jar",
183        "android/gradle/wrapper/gradle-wrapper.properties",
184        "android/gradle.properties",
185        "android/gradlew",
186        "android/gradlew.bat",
187        "android/settings.gradle",
188        "app.json",
189        "ios/.gitignore",
190        "ios/.xcode.env",
191        "ios/Podfile",
192        "ios/Podfile.properties.json",
193        "ios/basicprebuild/AppDelegate.h",
194        "ios/basicprebuild/AppDelegate.mm",
195        "ios/basicprebuild/Images.xcassets/AppIcon.appiconset/Contents.json",
196        "ios/basicprebuild/Images.xcassets/Contents.json",
197        "ios/basicprebuild/Images.xcassets/SplashScreenBackground.imageset/Contents.json",
198        "ios/basicprebuild/Images.xcassets/SplashScreenBackground.imageset/image.png",
199        "ios/basicprebuild/Info.plist",
200        "ios/basicprebuild/SplashScreen.storyboard",
201        "ios/basicprebuild/Supporting/Expo.plist",
202        "ios/basicprebuild/basicprebuild-Bridging-Header.h",
203        "ios/basicprebuild/basicprebuild.entitlements",
204        "ios/basicprebuild/main.m",
205        "ios/basicprebuild/noop-file.swift",
206        "ios/basicprebuild.xcodeproj/project.pbxproj",
207        "ios/basicprebuild.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
208        "ios/basicprebuild.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
209        "ios/basicprebuild.xcodeproj/xcshareddata/xcschemes/basicprebuild.xcscheme",
210        "package.json",
211        "yarn.lock",
212      ]
213    `);
214  },
215  // Could take 45s depending on how fast npm installs
216  60 * 1000
217);
218