xref: /expo/packages/create-expo/src/Examples.ts (revision b7d15820)
1import JsonFile from '@expo/json-file';
2import chalk from 'chalk';
3import fs from 'fs';
4import fetch from 'node-fetch';
5import path from 'path';
6import prompts from 'prompts';
7import { Stream } from 'stream';
8import tar from 'tar';
9import { promisify } from 'util';
10
11import { sanitizeTemplateAsync } from './Template';
12import { createEntryResolver, createFileTransform } from './createFileTransform';
13import { env } from './utils/env';
14
15const debug = require('debug')('expo:init:template') as typeof console.log;
16const pipeline = promisify(Stream.pipeline);
17
18/**
19 * The partial GitHub content type, used to filter out examples.
20 * @see https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28
21 */
22export type GithubContent = {
23  name: string;
24  path: string;
25  type: 'file' | 'dir';
26};
27
28/** List all existing examples directory from https://github.com/expo/examples. */
29async function listExamplesAsync() {
30  const response = await fetch('https://api.github.com/repos/expo/examples/contents');
31  if (!response.ok) {
32    throw new Error('Unexpected GitHub API response: https://github.com/expo/examples');
33  }
34
35  const data: GithubContent[] = await response.json();
36  return data.filter((item) => item.type === 'dir' && !item.name.startsWith('.'));
37}
38
39/** Determine if an example exists, using only its name */
40async function hasExampleAsync(name: string) {
41  const response = await fetch(
42    `https://api.github.com/repos/expo/examples/contents/${encodeURIComponent(name)}/package.json`
43  );
44
45  // Either ok or 404 responses are expected
46  if (response.status === 404 || response.ok) {
47    return response.ok;
48  }
49
50  throw new Error(`Unexpected GitHub API response: ${response.status} - ${response.statusText}`);
51}
52
53export async function ensureExampleExists(name: string) {
54  if (!(await hasExampleAsync(name))) {
55    throw new Error(`Example "${name}" does not exist, see https://github.com/expo/examples`);
56  }
57}
58
59/** Ask the user which example to create */
60export async function promptExamplesAsync() {
61  if (env.CI) {
62    throw new Error('Cannot prompt for examples in CI');
63  }
64
65  const examples = await listExamplesAsync();
66  const { answer } = await prompts({
67    type: 'select',
68    name: 'answer',
69    message: 'Choose an example:',
70    choices: examples.map((example) => ({
71      title: example.name,
72      value: example.path,
73    })),
74  });
75
76  if (!answer) {
77    console.log();
78    console.log(chalk`Please specify the example, example: {cyan --example with-router}`);
79    console.log();
80    process.exit(1);
81  }
82
83  return answer;
84}
85
86/** Download and move the selected example from https://github.com/expo/examples. */
87export async function downloadAndExtractExampleAsync(root: string, name: string) {
88  const projectName = path.basename(root);
89  const response = await fetch('https://codeload.github.com/expo/examples/tar.gz/master');
90  if (!response.ok) {
91    debug(`Failed to fetch the examples code, received status "${response.status}"`);
92    throw new Error('Failed to fetch the examples code from https://github.com/expo/examples');
93  }
94
95  await pipeline(
96    response.body,
97    tar.extract(
98      {
99        cwd: root,
100        transform: createFileTransform(projectName),
101        onentry: createEntryResolver(projectName),
102        strip: 2,
103      },
104      [`examples-master/${name}`]
105    )
106  );
107
108  await sanitizeTemplateAsync(root);
109  await sanitizeScriptsAsync(root);
110}
111
112function exampleHasNativeCode(root: string): boolean {
113  return [path.join(root, 'android'), path.join(root, 'ios')].some((folder) =>
114    fs.existsSync(folder)
115  );
116}
117
118export async function sanitizeScriptsAsync(root: string) {
119  const defaultScripts = exampleHasNativeCode(root)
120    ? {
121        start: 'expo start --dev-client',
122        android: 'expo run:android',
123        ios: 'expo run:ios',
124        web: 'expo start --web',
125      }
126    : {
127        start: 'expo start',
128        android: 'expo start --android',
129        ios: 'expo start --ios',
130        web: 'expo start --web',
131      };
132
133  const packageFile = new JsonFile(path.join(root, 'package.json'));
134  const packageJson = await packageFile.readAsync();
135
136  const scripts = (packageJson.scripts ?? {}) as Record<string, string>;
137  packageJson.scripts = { ...defaultScripts, ...scripts };
138
139  await packageFile.writeAsync(packageJson);
140}
141