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