import JsonFile from '@expo/json-file' ; import * as PackageManager from '@expo/package-manager' ; import spawnAsync from '@expo/spawn-async' ; import chalk from 'chalk' ; import { readdirSync } from 'fs-extra' ; import getenv from 'getenv' ; import got from 'got' ; import merge from 'lodash/merge' ; import ora from 'ora' ; import * as path from 'path' ; import { Stream } from 'stream' ; import tar from 'tar' ; import { promisify } from 'util' ; import Logger from './Logger' ; import { createFileTransform , createEntryResolver } from './createFileTransform' ; const pipeline = promisify ( Stream . pipeline ) ; const isMacOS = process . platform === 'darwin' ; const packageJSON = require ( '../package.json' ) ; export type PackageManagerName = 'yarn' | 'npm' ; /**
 * Extract a template app to a given file path and clean up any properties left over from npm to
 * prepare it for usage.
 */ export async function extractAndPrepareTemplateAppAsync ( projectRoot ) { const projectName = path . basename ( projectRoot ) ; await downloadAndExtractNpmModule ( projectRoot , 'expo-template-bare-minimum' , projectName ) ; const config = { name : projectName , expo : { name : projectName , slug : projectName , } , } ; let appFile = new JsonFile ( path . join ( projectRoot , 'app.json' ) ) ; let appJson = merge ( await appFile . readAsync ( ) , config ) ; await appFile . writeAsync ( appJson ) ; let packageFile = new JsonFile ( path . join ( projectRoot , 'package.json' ) ) ; let packageJson = await packageFile . readAsync ( ) ; const inputName = 'name' in config ? config . name : config . expo . name ; packageJson . name = sanitizeNpmPackageName ( inputName ) ; packageJson . version = '1.0.0' ; packageJson . private = true ; delete packageJson . description ; delete packageJson . tags ; delete packageJson . repository ; await packageFile . writeAsync ( packageJson ) ; return projectRoot ; } export function sanitizeNpmPackageName ( name ) { return ( applyKnownNpmPackageNameRules ( name ) || 'app' ) ; } function applyKnownNpmPackageNameRules ( name ) { while ( /^(\.|_)/ . test ( name ) ) { name = name . substring ( 1 ) ; } name = name . toLowerCase ( ) . replace ( /[^a-zA-Z._\-/@]/g , '' ) ; return ( name . normalize ( 'NFD' ) . replace ( /[\u0300-\u036f]/g , '' ) || null ) ; } export function validateName ( name ? ) { if ( typeof name !== 'string' || name === '' ) { return 'The project name can not be empty.' ; } if ( ! /^[a-z0-9@.\-_]+$/i . test ( name ) ) { return 'The project name can only contain URL-friendly characters.' ; } return true ; } export async function initGitRepoAsync ( root , flags = { silent : false } ) { try { await spawnAsync ( 'git' , [ 'rev-parse' , '--is-inside-work-tree' ] , { stdio : 'ignore' , cwd : root } ) ; ! flags . silent && Logger . gray ( 'New project is already inside of a Git repo, skipping git init.' ) ; } catch ( e ) { if ( e . errno === 'ENOENT' ) { ! flags . silent && Logger . gray ( 'Unable to initialize Git repo. `git` not in PATH.' ) ; return false ; } } try { await spawnAsync ( 'git' , [ 'init' ] , { stdio : 'ignore' , cwd : root } ) ; await spawnAsync ( 'git' , [ 'add' , '-A' ] , { stdio : 'ignore' , cwd : root } ) ; const commitMsg = `Initial commit\n\nGenerated by ${ packageJSON . name } ${ packageJSON . version }.` ; await spawnAsync ( 'git' , [ 'commit' , '-m' , commitMsg ] , { stdio : 'ignore' , cwd : root , } ) ; ! flags . silent && Logger . gray ( 'Initialized a Git repository.' ) ; return true ; } catch ( e ) { return false ; } } export async function installDependenciesAsync ( projectRoot , packageManager , flags = { silent : false } ) { const options = { cwd : projectRoot , silent : flags . silent } ; if ( packageManager === 'yarn' ) { const yarn = new PackageManager . YarnPackageManager ( options ) ; await yarn . installAsync ( ) ; } else { await new PackageManager . NpmPackageManager ( options ) . installAsync ( ) ; } } const tolerableFiles = [ '.DS_Store' , 'Thumbs.db' , '.git' , '.gitattributes' , '.gitignore' , '.npmignore' , '.travis.yml' , 'LICENSE' , 'docs' , '.idea' , 'npm-debug.log' , 'yarn-debug.log' , 'yarn-error.log' , ] ; export function getConflictsForDirectory ( projectRoot ) { return readdirSync ( projectRoot ) . filter ( ( file ) => ! ( /\.iml$/ . test ( file ) || tolerableFiles . includes ( file ) ) ) ; } export function logProjectReady ( { cdPath , packageManager , } ) { Logger . nested ( chalk . bold ( `✅ Your project is ready!` ) ) ; Logger . newLine ( ) ; if ( cdPath ) { Logger . nested ( `To run your project, navigate to the directory and run one of the following ${ packageManager } commands.` ) ; Logger . newLine ( ) ; Logger . nested ( `- ${ chalk . bold ( 'cd ' + cdPath ) }` ) ; } else { Logger . nested ( `To run your project, run one of the following ${ packageManager } commands.` ) ; Logger . newLine ( ) ; } Logger . nested ( `- ${ chalk . bold ( packageManager === 'npm' ? 'npm run android' : 'yarn android' ) }` ) ; let macOSComment = '' ; if ( ! isMacOS ) { macOSComment = ' # you need to use macOS to build the iOS project - use the Expo app if you need to do iOS development without a Mac' ; } Logger . nested ( `- ${ chalk . bold ( packageManager === 'npm' ? 'npm run ios' : 'yarn ios' ) }${ macOSComment }` ) ; Logger . nested ( `- ${ chalk . bold ( packageManager === 'npm' ? 'npm run web' : 'yarn web' ) }` ) ; } export async function installPodsAsync ( projectRoot ) { let step = logNewSection ( 'Installing CocoaPods.' ) ; if ( process . platform !== 'darwin' ) { step . succeed ( 'Skipped installing CocoaPods because operating system is not on macOS.' ) ; return false ; } const packageManager = new PackageManager . CocoaPodsPackageManager ( { cwd : path . join ( projectRoot , 'ios' ) , log : Logger . nested , silent : ! getenv . boolish ( 'EXPO_DEBUG' , false ) , } ) ; if ( ! ( await packageManager . isCLIInstalledAsync ( ) ) ) { try { step . text = 'CocoaPods CLI not found in your PATH, installing it now.' ; step . render ( ) ; await packageManager . installCLIAsync ( ) ; step . succeed ( 'Installed CocoaPods CLI' ) ; step = logNewSection ( 'Running `pod install` in the `ios` directory.' ) ; } catch ( e ) { step . stopAndPersist ( { symbol : '⚠️ ' , text : chalk . red ( 'Unable to install the CocoaPods CLI. Continuing with initializing the project, you can install CocoaPods afterwards.' ) , } ) ; if ( e . message ) { Logger . nested ( `- ${ e . message }` ) ; } return false ; } } try { await packageManager . installAsync ( ) ; step . succeed ( 'Installed pods and initialized Xcode workspace.' ) ; return true ; } catch ( e ) { step . stopAndPersist ( { symbol : '⚠️ ' , text : chalk . red ( 'Something went wrong running `pod install` in the `ios` directory. Continuing with initializing the project, you can debug this afterwards.' ) , } ) ; if ( e . message ) { Logger . nested ( `- ${ e . message }` ) ; } return false ; } } export function logNewSection ( title ) { let spinner = ora ( chalk . bold ( title ) ) ; spinner . start ( ) ; return spinner ; } async function getNpmUrlAsync ( packageName ) { const url = ( await spawnAsync ( 'npm' , [ 'v' , packageName , 'dist.tarball' ] ) ) . stdout ; if ( ! url ) { throw new Error ( `Could not get npm url for package "${ packageName }"` ) ; } return url ; } async function downloadAndExtractNpmModule ( root , npmName , projectName ) { const url = await getNpmUrlAsync ( npmName ) ; return pipeline ( got . stream ( url ) , tar . extract ( { cwd : root , transform : createFileTransform ( projectName ) , onentry : createEntryResolver ( projectName ) , strip : 1 , } , [ ] ) ) ; }