import template from 'mustache' ; import username from 'username' ; import chalk from 'chalk' ; import { v4 as uuidv4 } from 'uuid' ; import path from 'path' ; import fs from 'fs-extra' ; import os from 'os' ; import { flatten , omit , merge } from 'lodash' ; import { spawnSync } from 'child_process' ; import { PackageManager } from './package-manager' ; import { getVersion , sortKeys } from './utils' ; import { Dictionary , Options } from './types' ; type TemplateFolderName = 'javascript' | 'typescript' ; interface GeneratorOptions extends Options { projectName : string ; projectPath : string ; } const TYPESCRIPT_FOLDER = 'typescript' ; const JAVASCRIPT_FOLDER = 'javascript' ; const WINDOWS_FOLDER = 'windows' ; const COMMON_FOLDER = 'common' ; const IOS_FOLDER = 'ios' ; const TEMP_PACKAGE_JSON = '_package.json' ; const NPM_INSTALL = 'npm i' ; const NPM_RUN = 'npm run' ; const YARN = 'yarn' ; const TEMPLATE_PATH = path . resolve ( __dirname , '..' , 'template' ) ; const COMMON_TEMPLATE_PATH = path . join ( TEMPLATE_PATH , COMMON_FOLDER ) ; const WINDOWS_TEMPLATE_PATH = path . join ( COMMON_TEMPLATE_PATH , WINDOWS_FOLDER ) ; const WINDOWS_TEMPORARY_KEY_PATH = path . join ( WINDOWS_TEMPLATE_PATH , 'ProjectName' , 'ProjectName_TemporaryKey.pfx' ) ; const JEST_TEMPLATE_PATH = path . join ( COMMON_TEMPLATE_PATH , 'jest' ) ; const { chdir } = process ; export class Generator { private templateFolderName : TemplateFolderName ; private packageJson : Dictionary = { } ; private runCommand : string ; private options : GeneratorOptions ; private params : Dictionary = { } ; public constructor ( options ) { const { projectName , javascript , yarn } = options ; this . params = { ProjectName : projectName , displayName : projectName , projectname : projectName . toLowerCase ( ) , } ; this . templateFolderName = javascript ? JAVASCRIPT_FOLDER : TYPESCRIPT_FOLDER ; this . runCommand = yarn ? YARN : NPM_RUN ; this . options = options ; } public run ( ) { const { projectPath , skipInstall } = this . options ; console . log ( chalk . white . bold ( 'Setting up new ReactXP app in %s' ) , projectPath ) ; if ( ! fs . existsSync ( projectPath ) ) { fs . mkdirSync ( projectPath ) ; } this . setPackageJson ( ) ; this . generatePackageJson ( ) ; if ( ! skipInstall ) { this . installDependencies ( ) ; } this . generateApp ( ) ; this . generateWindowsApp ( ) ; this . printInstructions ( ) ; } private generateApp ( ) { const { skipJest , javascript } = this . options ; const templatePaths = [ COMMON_TEMPLATE_PATH , path . join ( TEMPLATE_PATH , this . templateFolderName ) , ] ; const excludePaths = [ WINDOWS_TEMPLATE_PATH , TEMP_PACKAGE_JSON ] ; if ( skipJest ) { excludePaths . push ( JEST_TEMPLATE_PATH ) ; excludePaths . push ( `App.spec.${ javascript ? 'js' : 'tsx' }` ) ; } const pathParams = { ... this . params , '_metro.config.js' : 'metro.config.js' , '_babel.config.js' : 'babel.config.js' , _eslintrc : '.eslintrc' , _gitignore : '.gitignore' , '_tsconfig.json' : 'tsconfig.json' , '_tslint.json' : 'tslint.json' , '_README.md' : 'README.md' , } ; const contnetParams = { ... this . params , runCommand : this . runCommand , version : getVersion ( ) , skipJest , } ; templatePaths . forEach ( ( srcPath ) => this . walk ( srcPath , excludePaths ) . forEach ( ( absolutePath ) => this . copy ( absolutePath , this . buildDestPath ( absolutePath , srcPath , pathParams ) , contnetParams ) ) ) ; } private generateWindowsApp ( ) { const currentUser = username . sync ( ) ; const certificateThumbprint = this . buildSelfSignedCertificate ( currentUser ) ; const contnetParams = { ... this . params , ... ( certificateThumbprint && { certificateThumbprint } ) , currentUser , packageGuid : uuidv4 ( ) , projectGuid : uuidv4 ( ) , } ; const pathParams = { ... this . params , _gitignore : '.gitignore' } ; const excludePaths = certificateThumbprint ? [ WINDOWS_TEMPORARY_KEY_PATH ] : [ ] ; this . walk ( WINDOWS_TEMPLATE_PATH , excludePaths ) . forEach ( ( absolutePath ) => this . copy ( absolutePath , this . buildDestPath ( absolutePath , COMMON_TEMPLATE_PATH , pathParams ) , contnetParams ) ) ; } private buildSelfSignedCertificate ( currentUser ) { const { projectPath , projectName } = this . options ; if ( os . platform ( ) !== 'win32' ) { return '' ; } console . log ( '%s %s' , chalk . blue . bold ( '[windows]' ) , chalk . white . bold ( 'Generating self-signed certificate...' ) ) ; const certificateDestPath = path . join ( projectPath , WINDOWS_FOLDER , projectName ) ; const certificateFileDestPath = path . join ( certificateDestPath , projectName ) ; const certificateArgs = [ [ '$cert = New-SelfSignedCertificate -KeyUsage DigitalSignature -KeyExportPolicy Exportable -Subject' , `"CN=${ currentUser }" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}Subject Type:End Entity")` , `-CertStoreLocation "Cert:\\CurrentUser\\My"` , ] . join ( ' ' ) , '$pwd = ConvertTo-SecureString -String password -Force -AsPlainText' , `New-Item -ErrorAction Ignore -ItemType directory -Path ${ certificateDestPath }` , [ `Export-PfxCertificate -Cert "cert:\\CurrentUser\\My\\$($cert.Thumbprint)"` , `-FilePath ${ certificateFileDestPath }_TemporaryKey.pfx -Password $pwd` , ] . join ( ' ' ) , '$cert.Thumbprint' , ] . join ( ';' ) ; if ( ! fs . existsSync ( certificateDestPath ) ) { fs . mkdirpSync ( certificateDestPath ) ; } const { status , stdout } = spawnSync ( 'powershell' , [ '-command' , certificateArgs , ] ) ; if ( status === 0 ) { console . log ( '%s %s' , chalk . blue . bold ( '[windows]' ) , chalk . green . bold ( 'Self-signed certificate generated successfully.' ) ) ; const output = stdout . toString ( ) . trim ( ) . split ( '\n' ) ; return output [ output . length - 1 ] ; } console . log ( '%s %s' , chalk . blue . bold ( '[windows]' ) , chalk . red . bold ( 'Failed to generate Self-signed certificate.' ) ) ; console . log ( '%s %s' , chalk . blue . bold ( '[windows]' ) , chalk . yellow . bold ( 'Using Default Certificate. Use Visual Studio to renew it.' ) ) ; return '' ; } private generatePackageJson ( ) { const { projectPath , projectName , skipInstall } = this . options ; const packageJson = JSON . stringify ( { name : projectName . toLowerCase ( ) , ... omit ( this . packageJson , skipInstall ? [ ] : [ 'devDependencies' , 'dependencies' ] ) , } , null , 2 ) ; fs . writeFileSync ( path . resolve ( projectPath , 'package.json' ) , packageJson ) ; } private printInstructions ( ) { const { projectName , projectPath , skipInstall , yarn } = this . options ; const xcodeProjectPath = `${ path . resolve ( projectPath , IOS_FOLDER , projectName ) }.xcodeproj` ; const windowsProjectPath = `${ path . resolve ( projectPath , WINDOWS_FOLDER , projectName ) }.sln` ; console . log ( chalk . green . bold ( '%s was successfully created. \n' ) , projectName ) ; if ( skipInstall ) { console . log ( chalk . green . bold ( 'To install dependencies:' ) ) ; console . log ( '  cd %s' , projectPath ) ; console . log ( chalk . white . bold ( '  %s \n' ) , yarn ? YARN : NPM_INSTALL ) ; } console . log ( chalk . yellow . bold ( 'To run your app on Web:' ) ) ; console . log ( '  cd %s' , projectPath ) ; console . log ( chalk . white . bold ( '  %s start:web \n' ) , this . runCommand ) ; console . log ( chalk . yellow . bold ( 'To build Web production version of your app:' ) ) ; console . log ( '  cd %s' , projectPath ) ; console . log ( chalk . white . bold ( '  %s build:web \n' ) , this . runCommand ) ; console . log ( chalk . cyan . bold ( 'To run your app on iOS:' ) ) ; console . log ( '  cd %s' , projectPath ) ; console . log ( chalk . white . bold ( '  %s start:ios' ) , this . runCommand ) ; console . log ( '  - or -' ) ; console . log ( '  open %s project in Xcode' , path . relative ( projectPath , xcodeProjectPath ) ) ; console . log ( '  press the Run button \n' ) ; console . log ( chalk . green . bold ( 'To run your app on Android:' ) ) ; console . log ( '  cd %s' , projectPath ) ; console . log ( '  Have an Android emulator running (quickest way to get started), or a device connected.' ) ; console . log ( chalk . white . bold ( '  %s start:android' ) , this . runCommand ) ; console . log ( '  - or -' ) ; console . log ( chalk . white . bold ( '  open android/ project in Android Studio' ) ) ; console . log ( '  press the Run button \n' ) ; console . log ( chalk . blue . bold ( 'To run your app on Windows:' ) ) ; console . log ( '  cd %s' , projectPath ) ; console . log ( chalk . white . bold ( '  %s start:windows' ) , this . runCommand ) ; console . log ( '  - or -' ) ; console . log ( chalk . white . bold ( '  open %s project in Visual Studio' ) , windowsProjectPath ) ; console . log ( '  press the Run button \n' ) ; } private installDependencies ( ) { const { devDependencies , dependencies } = this . packageJson ; const { projectPath , yarn } = this . options ; const packageManager = new PackageManager ( yarn ) ; chdir ( projectPath ) ; packageManager . install ( devDependencies , 'dev dependencies' , true ) ; packageManager . install ( dependencies , 'dependencies' ) ; } private setPackageJson ( ) { const packageJson = merge ( this . buildPackageJson ( COMMON_FOLDER ) , this . buildPackageJson ( this . templateFolderName ) ) ; this . packageJson = { ... packageJson , devDependencies : sortKeys ( packageJson . devDependencies ) , dependencies : sortKeys ( packageJson . dependencies ) , } ; } private buildPackageJson ( srcFolder ) { const { skipJest , yarn } = this . options ; const content = fs . readFileSync ( path . join ( TEMPLATE_PATH , srcFolder , TEMP_PACKAGE_JSON ) , { encoding : 'utf8' } ) ; const params = { paramsPrefix : yarn ? ' ' : ' -- ' , runCommand : this . runCommand , skipJest , } ; return JSON . parse ( template . render ( content , params , { } , [ '<%' , '%>' ] ) ) ; } private buildDestPath ( absolutePath , relativeTo , params ) { let destPath = path . resolve ( this . options . projectPath , path . relative ( relativeTo , absolutePath ) ) ; Object . keys ( params ) . forEach ( ( regexp ) => ( destPath = destPath . replace ( new RegExp ( `\\b${ regexp }` , 'g' ) , params [ regexp ] ) ) ) ; return destPath ; } private copy ( srcPath , destPath , params ) { if ( fs . lstatSync ( srcPath ) . isDirectory ( ) ) { if ( ! fs . existsSync ( destPath ) ) { fs . mkdirSync ( destPath ) ; } return ; } else { if ( this . isBinary ( srcPath ) ) { fs . copyFileSync ( srcPath , destPath ) ; } else { const permissions = fs . statSync ( srcPath ) . mode ; const content = template . render ( fs . readFileSync ( srcPath , 'utf8' ) , params , { } , [ '<%' , '%>' ] ) ; fs . writeFileSync ( destPath , content , { encoding : 'utf8' , mode : permissions , } ) ; } } } private isBinary ( srcPath ) { return ( [ '.png' , '.jpg' , '.jar' , '.ico' , '.pfx' , '.keystore' ] . indexOf ( path . extname ( srcPath ) ) >= 0 ) ; } private walk ( srcPath , excludePaths = [ ] ) { const isExcluded = ( file ) => excludePaths . some ( ( p ) => file . indexOf ( p ) >= 0 ) ; if ( ! fs . lstatSync ( srcPath ) . isDirectory ( ) ) { return isExcluded ( srcPath ) ? [ ] : [ srcPath ] ; } return [ srcPath , ... flatten ( fs . readdirSync ( srcPath ) . map ( ( child ) => this . walk ( path . join ( srcPath , child ) ) ) ) , ] . filter ( ( absolutePath ) => ! isExcluded ( absolutePath ) ) ; } }