1ca5a2fa2STomasz Sapetaimport spawnAsync from '@expo/spawn-async';
2ca5a2fa2STomasz Sapetaimport fs from 'fs-extra';
3c4c29f98SBrent Vatneimport getenv from 'getenv';
40bc2d977SJaakko Nakazaimport os from 'os';
5ca5a2fa2STomasz Sapetaimport path from 'path';
6ca5a2fa2STomasz Sapeta
7ca5a2fa2STomasz Sapetaimport { installDependencies } from './packageManager';
8ca5a2fa2STomasz Sapetaimport { PackageManagerName } from './resolvePackageManager';
9ca5a2fa2STomasz Sapetaimport { SubstitutionData } from './types';
10234d009cSTomasz Sapetaimport { newStep } from './utils';
11ca5a2fa2STomasz Sapeta
12c4c29f98SBrent Vatneconst debug = require('debug')('create-expo-module:createExampleApp') as typeof console.log;
13c4c29f98SBrent Vatne
14ca5a2fa2STomasz Sapeta// These dependencies will be removed from the example app (`expo init` adds them)
15ca5a2fa2STomasz Sapetaconst DEPENDENCIES_TO_REMOVE = ['expo-status-bar', 'expo-splash-screen'];
16c4c29f98SBrent Vatneconst EXPO_BETA = getenv.boolish('EXPO_BETA', false);
17ca5a2fa2STomasz Sapeta
18ca5a2fa2STomasz Sapeta/**
19ca5a2fa2STomasz Sapeta * Initializes a new Expo project as an example app.
20ca5a2fa2STomasz Sapeta */
21ca5a2fa2STomasz Sapetaexport async function createExampleApp(
22ca5a2fa2STomasz Sapeta  data: SubstitutionData,
23ca5a2fa2STomasz Sapeta  targetDir: string,
24ca5a2fa2STomasz Sapeta  packageManager: PackageManagerName
25ca5a2fa2STomasz Sapeta): Promise<void> {
2650ef9c7cSTomasz Sapeta  // Package name for the example app
27ca5a2fa2STomasz Sapeta  const exampleProjectSlug = `${data.project.slug}-example`;
28ca5a2fa2STomasz Sapeta
2950ef9c7cSTomasz Sapeta  // `expo init` creates a new folder with the same name as the project slug
3050ef9c7cSTomasz Sapeta  const appTmpPath = path.join(targetDir, exampleProjectSlug);
3150ef9c7cSTomasz Sapeta
3250ef9c7cSTomasz Sapeta  // Path to the target example dir
3350ef9c7cSTomasz Sapeta  const appTargetPath = path.join(targetDir, 'example');
3450ef9c7cSTomasz Sapeta
3550ef9c7cSTomasz Sapeta  if (!(await fs.pathExists(appTargetPath))) {
3650ef9c7cSTomasz Sapeta    // The template doesn't include the example app, so just skip this phase
3750ef9c7cSTomasz Sapeta    return;
3850ef9c7cSTomasz Sapeta  }
3950ef9c7cSTomasz Sapeta
40234d009cSTomasz Sapeta  await newStep('Initializing the example app', async (step) => {
41c4c29f98SBrent Vatne    const templateVersion = EXPO_BETA ? 'next' : 'latest';
42c4c29f98SBrent Vatne    const template = `expo-template-blank-typescript@${templateVersion}`;
43c4c29f98SBrent Vatne    debug(`Using example template: ${template}`);
44ca5a2fa2STomasz Sapeta    await spawnAsync(
45a2ce1fb6STomasz Sapeta      packageManager,
46c4c29f98SBrent Vatne      ['create', 'expo-app', '--', exampleProjectSlug, '--template', template, '--yes'],
47ca5a2fa2STomasz Sapeta      {
48ca5a2fa2STomasz Sapeta        cwd: targetDir,
49a2ce1fb6STomasz Sapeta        stdio: 'ignore',
50ca5a2fa2STomasz Sapeta      }
51ca5a2fa2STomasz Sapeta    );
52234d009cSTomasz Sapeta    step.succeed('Initialized the example app');
53234d009cSTomasz Sapeta  });
54ca5a2fa2STomasz Sapeta
55234d009cSTomasz Sapeta  await newStep('Configuring the example app', async (step) => {
56ca5a2fa2STomasz Sapeta    // "example" folder already exists and contains template files,
57ca5a2fa2STomasz Sapeta    // that should replace these created by `expo init`.
58ca5a2fa2STomasz Sapeta    await moveFiles(appTargetPath, appTmpPath);
59ca5a2fa2STomasz Sapeta
60ca5a2fa2STomasz Sapeta    // Cleanup the "example" dir
61ca5a2fa2STomasz Sapeta    await fs.rmdir(appTargetPath);
62ca5a2fa2STomasz Sapeta
6318522c87SCedric van Putten    // Clean up the ".git" from example app
6418522c87SCedric van Putten    // note, this directory has contents, rmdir will throw
6518522c87SCedric van Putten    await fs.remove(path.join(appTmpPath, '.git'));
6618522c87SCedric van Putten
67ca5a2fa2STomasz Sapeta    // Move the temporary example app to "example" dir
68ca5a2fa2STomasz Sapeta    await fs.rename(appTmpPath, appTargetPath);
69ca5a2fa2STomasz Sapeta
70ca5a2fa2STomasz Sapeta    await addMissingAppConfigFields(appTargetPath, data);
71ca5a2fa2STomasz Sapeta
72234d009cSTomasz Sapeta    step.succeed('Configured the example app');
73234d009cSTomasz Sapeta  });
74234d009cSTomasz Sapeta
75ca5a2fa2STomasz Sapeta  await prebuildExampleApp(appTargetPath);
76ca5a2fa2STomasz Sapeta
77ca5a2fa2STomasz Sapeta  await modifyPackageJson(appTargetPath);
78ca5a2fa2STomasz Sapeta
79234d009cSTomasz Sapeta  await newStep('Installing dependencies in the example app', async (step) => {
80ca5a2fa2STomasz Sapeta    await installDependencies(packageManager, appTargetPath);
810bc2d977SJaakko Nakaza    if (os.platform() === 'darwin') {
82ca5a2fa2STomasz Sapeta      await podInstall(appTargetPath);
83234d009cSTomasz Sapeta      step.succeed('Installed dependencies in the example app');
840bc2d977SJaakko Nakaza    } else {
850bc2d977SJaakko Nakaza      step.succeed('Installed dependencies in the example app (skipped installing CocoaPods)');
860bc2d977SJaakko Nakaza    }
87234d009cSTomasz Sapeta  });
88ca5a2fa2STomasz Sapeta}
89ca5a2fa2STomasz Sapeta
90ca5a2fa2STomasz Sapeta/**
91ca5a2fa2STomasz Sapeta * Copies files from one directory to another.
92ca5a2fa2STomasz Sapeta */
93ca5a2fa2STomasz Sapetaasync function moveFiles(fromPath: string, toPath: string): Promise<void> {
94ca5a2fa2STomasz Sapeta  for (const file of await fs.readdir(fromPath)) {
95ca5a2fa2STomasz Sapeta    await fs.move(path.join(fromPath, file), path.join(toPath, file), {
96ca5a2fa2STomasz Sapeta      overwrite: true,
97ca5a2fa2STomasz Sapeta    });
98ca5a2fa2STomasz Sapeta  }
99ca5a2fa2STomasz Sapeta}
100ca5a2fa2STomasz Sapeta
101ca5a2fa2STomasz Sapeta/**
102*384598e2SBrent Vatne * Adds missing configuration that are required to run `npx expo prebuild`.
103ca5a2fa2STomasz Sapeta */
104ca5a2fa2STomasz Sapetaasync function addMissingAppConfigFields(appPath: string, data: SubstitutionData): Promise<void> {
105ca5a2fa2STomasz Sapeta  const appConfigPath = path.join(appPath, 'app.json');
106ca5a2fa2STomasz Sapeta  const appConfig = await fs.readJson(appConfigPath);
107ca5a2fa2STomasz Sapeta  const appId = `${data.project.package}.example`;
108ca5a2fa2STomasz Sapeta
109ca5a2fa2STomasz Sapeta  // Android package name needs to be added to app.json
110ca5a2fa2STomasz Sapeta  if (!appConfig.expo.android) {
111ca5a2fa2STomasz Sapeta    appConfig.expo.android = {};
112ca5a2fa2STomasz Sapeta  }
113ca5a2fa2STomasz Sapeta  appConfig.expo.android.package = appId;
114ca5a2fa2STomasz Sapeta
115ca5a2fa2STomasz Sapeta  // Specify iOS bundle identifier
116ca5a2fa2STomasz Sapeta  if (!appConfig.expo.ios) {
117ca5a2fa2STomasz Sapeta    appConfig.expo.ios = {};
118ca5a2fa2STomasz Sapeta  }
119ca5a2fa2STomasz Sapeta  appConfig.expo.ios.bundleIdentifier = appId;
120ca5a2fa2STomasz Sapeta
121ca5a2fa2STomasz Sapeta  await fs.writeJson(appConfigPath, appConfig, {
122ca5a2fa2STomasz Sapeta    spaces: 2,
123ca5a2fa2STomasz Sapeta  });
124ca5a2fa2STomasz Sapeta}
125ca5a2fa2STomasz Sapeta
126ca5a2fa2STomasz Sapeta/**
127ca5a2fa2STomasz Sapeta * Applies necessary changes to **package.json** of the example app.
128ca5a2fa2STomasz Sapeta * It means setting the autolinking config and removing unnecessary dependencies.
129ca5a2fa2STomasz Sapeta */
130ca5a2fa2STomasz Sapetaasync function modifyPackageJson(appPath: string): Promise<void> {
131ca5a2fa2STomasz Sapeta  const packageJsonPath = path.join(appPath, 'package.json');
132ca5a2fa2STomasz Sapeta  const packageJson = await fs.readJson(packageJsonPath);
133ca5a2fa2STomasz Sapeta
134ca5a2fa2STomasz Sapeta  if (!packageJson.expo) {
135ca5a2fa2STomasz Sapeta    packageJson.expo = {};
136ca5a2fa2STomasz Sapeta  }
137ca5a2fa2STomasz Sapeta
138ca5a2fa2STomasz Sapeta  // Set the native modules dir to the root folder,
139ca5a2fa2STomasz Sapeta  // so that the autolinking can detect and link the module.
140ca5a2fa2STomasz Sapeta  packageJson.expo.autolinking = {
141ca5a2fa2STomasz Sapeta    nativeModulesDir: '..',
142ca5a2fa2STomasz Sapeta  };
143ca5a2fa2STomasz Sapeta
144ca5a2fa2STomasz Sapeta  // Remove unnecessary dependencies
145ca5a2fa2STomasz Sapeta  for (const dependencyToRemove of DEPENDENCIES_TO_REMOVE) {
146ca5a2fa2STomasz Sapeta    delete packageJson.dependencies[dependencyToRemove];
147ca5a2fa2STomasz Sapeta  }
148ca5a2fa2STomasz Sapeta
149ca5a2fa2STomasz Sapeta  await fs.writeJson(packageJsonPath, packageJson, {
150ca5a2fa2STomasz Sapeta    spaces: 2,
151ca5a2fa2STomasz Sapeta  });
152ca5a2fa2STomasz Sapeta}
153ca5a2fa2STomasz Sapeta
154ca5a2fa2STomasz Sapeta/**
155fb08e93cSBartosz Kaszubowski * Runs `npx expo prebuild` in the example app.
156ca5a2fa2STomasz Sapeta */
157ca5a2fa2STomasz Sapetaasync function prebuildExampleApp(exampleAppPath: string): Promise<void> {
158234d009cSTomasz Sapeta  await newStep('Prebuilding the example app', async (step) => {
159fb08e93cSBartosz Kaszubowski    await spawnAsync('npx', ['expo', 'prebuild', '--no-install'], {
160ca5a2fa2STomasz Sapeta      cwd: exampleAppPath,
161ca5a2fa2STomasz Sapeta      stdio: ['ignore', 'ignore', 'pipe'],
162ca5a2fa2STomasz Sapeta    });
163234d009cSTomasz Sapeta    step.succeed('Prebuilt the example app');
164234d009cSTomasz Sapeta  });
165ca5a2fa2STomasz Sapeta}
166ca5a2fa2STomasz Sapeta
167ca5a2fa2STomasz Sapeta/**
168ca5a2fa2STomasz Sapeta * Runs `pod install` in the iOS project at the given path.
169ca5a2fa2STomasz Sapeta */
170ca5a2fa2STomasz Sapetaasync function podInstall(appPath: string): Promise<void> {
171ca5a2fa2STomasz Sapeta  await spawnAsync('pod', ['install'], {
172ca5a2fa2STomasz Sapeta    cwd: path.join(appPath, 'ios'),
173ca5a2fa2STomasz Sapeta    stdio: ['ignore', 'ignore', 'pipe'],
174ca5a2fa2STomasz Sapeta  });
175ca5a2fa2STomasz Sapeta}
176