/**
 * Inspired by create-next-app
 */ import JsonFile , { JSONObject } from '@expo/json-file' ; import chalk from 'chalk' ; import fs from 'fs' ; import got from 'got' ; import path from 'path' ; import prompts from 'prompts' ; import { Stream } from 'stream' ; import tar from 'tar' ; import terminalLink from 'terminal-link' ; import { promisify } from 'util' ; import { sanitizeNpmPackageName } from './Template' ; import { createFileTransform , createEntryResolver } from './createFileTransform' ; const pipeline = promisify ( Stream . pipeline ) ; type RepoInfo = { username : string ; name : string ; branch : string ; filePath : string ; } ; export async function promptAsync ( ) { const { value } = await prompts ( { type : 'select' , name : 'value' , limit : 11 , message : 'How would you like to start' , choices : [ { title : 'Default new app' , value : 'default' } , { title : `Template from ${ terminalLink ( 'expo/examples' , 'https://github.com/expo/examples' , { fallback : ( text , url ) => `${ text }: ${ url }` , } ) }` , value : 'example' , } , ] , } ) ; if ( ! value ) { console . log ( ) ; console . log ( 'Please specify the template' ) ; process . exit ( 1 ) ; } if ( value === 'example' ) { let examplesJSON ; try { examplesJSON = await listAsync ( ) ; } catch ( error ) { console . log ( ) ; console . log ( 'Failed to fetch the list of examples with the following error:' ) ; console . error ( error ) ; console . log ( ) ; console . log ( 'Switching to the default starter app' ) ; console . log ( ) ; } if ( examplesJSON ) { const choices = examplesJSON . map ( ( { name } ) => ( { title : name , value : name , } ) ) ; const nameRes = await prompts ( { type : 'autocomplete' , name : 'exampleName' , message : 'Pick an example' , choices , suggest : ( input , choices ) => { const regex = new RegExp ( input , 'i' ) ; return choices . filter ( ( choice ) => regex . test ( choice . title ) ) ; } , } ) ; if ( ! nameRes . exampleName ) { console . log ( ) ; console . log ( 'Please specify an example or use the default starter app.' ) ; process . exit ( 1 ) ; } return nameRes . exampleName . trim ( ) ; } } return null ; } async function isUrlOk ( url ) { const res = await got ( url ) . catch ( e => e ) ; return res . statusCode === 200 ; } async function getRepoInfo ( url , examplePath ? ) { const [ , username , name , t , _branch , ... file ] = url . pathname . split ( '/' ) ; const filePath = examplePath ? examplePath . replace ( /^\// , '' ) : file . join ( '/' ) ; if ( t === undefined ) { const infoResponse = await got ( `https://api.github.com/repos/${ username }/${ name }` ) . catch ( e => e ) ; if ( infoResponse . statusCode !== 200 ) { return ; } const info = JSON . parse ( infoResponse . body ) ; return { username , name , branch : info [ 'default_branch' ] , filePath } ; } const branch = examplePath ? `${ _branch }/${ file . join ( '/' ) }` . replace ( new RegExp ( `/${ filePath }|/$` ) , '' ) : _branch ; if ( username && name && branch && t === 'tree' ) { return { username , name , branch , filePath } ; } return undefined ; } function hasRepo ( { username , name , branch , filePath } ) { const contentsUrl = `https://api.github.com/repos/${ username }/${ name }/contents` ; const packagePath = `${ filePath ? `/${ filePath }` : '' }/package.json` ; return isUrlOk ( contentsUrl + packagePath + `?ref=${ branch }` ) ; } export async function resolveTemplateArgAsync ( projectRoot , oraInstance , template , templatePath ? ) { let repoInfo ; if ( template ) { let repoUrl ; try { repoUrl = new URL ( template ) ; } catch ( error ) { if ( error . code !== 'ERR_INVALID_URL' ) { oraInstance . fail ( error ) ; process . exit ( 1 ) ; } } if ( repoUrl ) { if ( repoUrl . origin !== 'https://github.com' ) { oraInstance . fail ( `Invalid URL: ${ chalk . red ( `"${ template }"` ) }. Only GitHub repositories are supported. Please use a GitHub URL and try again.` ) ; process . exit ( 1 ) ; } repoInfo = await getRepoInfo ( repoUrl , templatePath ) ; if ( ! repoInfo ) { oraInstance . fail ( `Found invalid GitHub URL: ${ chalk . red ( `"${ template }"` ) }. Please fix the URL and try again.` ) ; process . exit ( 1 ) ; } const found = await hasRepo ( repoInfo ) ; if ( ! found ) { oraInstance . fail ( `Could not locate the repository for ${ chalk . red ( `"${ template }"` ) }. Please check that the repository exists and try again.` ) ; process . exit ( 1 ) ; } } else { const found = await hasExample ( template ) ; if ( ! found ) { oraInstance . fail ( `Could not locate the template named ${ chalk . red ( `"${ template }"` ) }.` ) ; process . exit ( 1 ) ; } } } if ( repoInfo ) { oraInstance . text = chalk . bold ( `Downloading files from repo ${ chalk . cyan ( template ) }. This might take a moment.` ) ; await downloadAndExtractRepoAsync ( projectRoot , repoInfo ) ; } else { oraInstance . text = chalk . bold ( `Downloading files for example ${ chalk . cyan ( template ) }. This might take a moment.` ) ; await downloadAndExtractExampleAsync ( projectRoot , template ) ; } await ensureProjectHasGitIgnore ( projectRoot ) ; return true ; } function projectHasNativeCode ( projectRoot ) { const iosPath = path . join ( projectRoot , 'ios' ) ; const androidPath = path . join ( projectRoot , 'android' ) ; return fs . existsSync ( iosPath ) || fs . existsSync ( androidPath ) ; } function getScriptsForProject ( projectRoot ) { if ( projectHasNativeCode ( projectRoot ) ) { return { android : 'expo run:android' , ios : 'expo run:ios' , web : 'expo start --web' , start : 'expo start --dev-client' , } ; } return { start : 'expo start' , android : 'expo start --android' , ios : 'expo start --ios' , web : 'expo start --web' , eject : 'expo eject' , } ; } export async function appendScriptsAsync ( projectRoot ) { const packageJsonPath = path . join ( projectRoot , 'package.json' ) ; if ( fs . existsSync ( packageJsonPath ) ) { let packageFile = new JsonFile ( packageJsonPath ) ; let packageJson = await packageFile . readAsync ( ) ; packageJson = { ... packageJson , scripts : { ... getScriptsForProject ( projectRoot ) , ... ( ( packageJson . scripts || { } ) as JSONObject ) , } , version : '1.0.0' , private : true , } ; packageJson . name = sanitizeNpmPackageName ( path . basename ( projectRoot ) ) ; await packageFile . writeAsync ( packageJson ) ; } } function ensureProjectHasGitIgnore ( projectRoot ) { const ignorePath = path . join ( projectRoot , '.gitignore' ) ; if ( ! fs . existsSync ( ignorePath ) ) { fs . copyFileSync ( require . resolve ( '../template/gitignore' ) , ignorePath ) ; } } function hasExample ( name ) { return isUrlOk ( `https://api.github.com/repos/expo/examples/contents/${ encodeURIComponent ( name ) }/package.json` ) ; } function downloadAndExtractRepoAsync ( root , { username , name , branch , filePath } ) { const projectName = path . basename ( root ) ; const strip = filePath ? filePath . split ( '/' ) . length + 1 : 1 ; return pipeline ( got . stream ( `https://codeload.github.com/${ username }/${ name }/tar.gz/${ branch }` ) , tar . extract ( { cwd : root , transform : createFileTransform ( projectName ) , onentry : createEntryResolver ( projectName ) , strip , } , [ `${ name }-${ branch }${ filePath ? `/${ filePath }` : '' }` ] ) ) ; } function downloadAndExtractExampleAsync ( root , name ) { const projectName = path . basename ( root ) ; return pipeline ( got . stream ( 'https://codeload.github.com/expo/examples/tar.gz/master' ) , tar . extract ( { cwd : root , transform : createFileTransform ( projectName ) , onentry : createEntryResolver ( projectName ) , strip : 2 , } , [ `examples-master/${ name }` ] ) ) ; } async function listAsync ( ) { const res = await got ( 'https://api.github.com/repos/expo/examples/contents' ) ; const results = JSON . parse ( res . body ) ; return results . filter ( ( { name , type } ) => type === 'dir' && ! name ?. startsWith ( '.' ) ) ; }