import fs , { copyFileSync , createReadStream , existsSync , unlinkSync , writeFileSync , statSync } from 'fs' ; import * as path from 'path' ; import os , { homedir } from 'os' ; import { promisify } from 'util' ; import crypto from 'crypto' ; import { sync as globbySync } from 'globby' ; import mkdirp from 'mkdirp' ; import rimRaf from 'rimraf' ; import appModule from 'app-module-path' ; import { confirm } from './questions' ; const flexUI = '@twilio/flex-ui' ; const react = 'react' ; const reactDOM = 'react-dom' ; export interface PackageJson { name : string ; version : string ; dependencies : Record < string , string > ; devDependencies : Record < string , string > ; peerDependencies ? : Record < string , string > ; } export interface AppPackageJson extends PackageJson { dependencies : { 'flex-plugin' : string ; 'flex-plugin-scripts' : string ; } ; } export interface FlexConfigurationPlugin { name : string ; dir : string ; } export interface CLIFlexConfiguration { plugins : FlexConfigurationPlugin [ ] ; } export interface LocallyRunningPluginsConfiguration { plugins : string [ ] ; loadedPlugins : string [ ] ; } export type JsonObject < T > = { [ K in keyof T ] : T [ K ] ; } ; export default fs ; const packageJsonStr = 'package.json' ; param filePath private export const _require = ( filePath ) => require ( filePath ) ; export const _setRequirePaths = ( requirePath ) : void => { appModule . addPath ( requirePath ) ; const requirePaths = ( require . main && require . main . paths ) || [ ] ; if ( ! requirePaths . includes ( requirePath ) ) { requirePaths . push ( requirePath ) ; } } ; param filePath export const readPackageJson = ( filePath ) : PackageJson => { return JSON . parse ( fs . readFileSync ( filePath , 'utf8' ) ) ; } ; param filePaths export const getSileSizeInBytes = ( ... filePaths ) : number => statSync ( path . join ( ... filePaths ) ) . size ; param filePaths export const getFileSizeInMB = ( ... filePaths ) : number => getSileSizeInBytes ( ... filePaths ) / ( 1024 * 1024 ) ; param dir param paths export const resolveRelative = ( dir , ... paths ) : string => { if ( paths . length === 0 ) { return dir ; } const lastElement = paths [ paths . length - 1 ] ; if ( lastElement . charAt ( 0 ) !== '.' ) { return path . join ( dir , ... paths ) ; } if ( paths . length === 1 ) { return path . join ( `${ dir }${ lastElement }` ) ; } const secondLastElement = paths [ paths . length - 2 ] ; const remainder = paths . slice ( 0 , paths . length - 2 ) ; return path . join ( dir , ... [ ... remainder , `${ secondLastElement }${ lastElement }` ] ) ; } ; let internalCwd = fs . realpathSync ( process . cwd ( ) ) ; let internalCoreCwd = fs . realpathSync ( process . cwd ( ) ) ; param p export const setCwd = ( p ) : void => { internalCwd = p ; _setRequirePaths ( path . join ( internalCwd , 'node_modules' ) ) ; } ; /**
 * Returns the working directory
 */ export const getCwd = ( ) : string => internalCwd ; param p export const setCoreCwd = ( p ) : void => { internalCoreCwd = p ; _setRequirePaths ( path . join ( internalCoreCwd , 'node_modules' ) ) ; } ; /**
 * The core cwd is the working directory of core packages such as flex-plugin-scripts and flex-plugin
 */ export const getCoreCwd = ( ) : string => internalCoreCwd ; param filePaths export const readFileSync = ( ... filePaths ) : string => fs . readFileSync ( path . join ( ... filePaths ) , 'utf8' ) ; param filePaths export const readJsonFile = < T > ( ... filePaths ) : T => { return JSON . parse ( readFileSync ( ... filePaths ) ) ; } ; /**
 * Gets the CLI paths. This is separated out from getPaths because create-flex-plugin also needs to read it,
 * but that script will not have flex-plugin-scripts installed which would cause an exception to be thrown.
 */ export const getCliPaths = ( ) => { const coreCwd = getCoreCwd ( ) ; const coreNodeModulesDir = resolveRelative ( coreCwd , 'node_modules' ) ; const homeDir = homedir ( ) ; const cliDir = resolveRelative ( homeDir , '/.twilio-cli' ) ; const flexDir = resolveRelative ( cliDir , 'flex' ) ; return { dir : cliDir , nodeModulesDir : coreNodeModulesDir , flexDir , pluginsJsonPath : resolveRelative ( flexDir , 'plugins.json' ) , localPluginsJsonPath : resolveRelative ( flexDir , 'locallyRunningPlugins.json' ) , } ; } ; export const readPluginsJson = ( ) : CLIFlexConfiguration => readJsonFile < CLIFlexConfiguration > ( getCliPaths ( ) . pluginsJsonPath ) ; export const readRunPluginsJson = ( ) : LocallyRunningPluginsConfiguration => readJsonFile < LocallyRunningPluginsConfiguration > ( getCliPaths ( ) . localPluginsJsonPath ) ; /**
 * Writes string to file
 */ export const writeFile = ( str , ... paths ) : void => writeFileSync ( path . join ( ... paths ) , str ) ; param obj param paths export const writeJSONFile = < T > ( obj , ... paths ) : void => writeFile ( JSON . stringify ( obj , null , 2 ) , ... paths ) ; const rootDir = os . platform ( ) === 'win32' ? getCwd ( ) . split ( path . sep ) [ 0 ] : '/' ; const promiseCopyTempDir = promisify ( _require ( 'copy-template-dir' ) ) ; param files export const checkFilesExist = ( ... files ) : boolean => { return files . map ( fs . existsSync ) . every ( ( resp ) => resp ) ; } ; param paths export const calculateSha256 = async ( ... paths ) : Promise < string > => { return new Promise ( ( resolve , reject ) => { const shasum = crypto . createHash ( 'sha256' ) ; const stream = createReadStream ( path . join ( ... paths ) ) ; stream . on ( 'data' , ( data ) => shasum . update ( data ) ) ; stream . on ( 'error' , ( err ) => reject ( err ) ) ; stream . on ( 'end' , ( ) => resolve ( shasum . digest ( 'hex' ) ) ) ; } ) ; } ; param paths export const removeFile = ( ... paths ) : void => unlinkSync ( path . join ( ... paths ) ) ; param srcPaths param destPaths export const copyFile = ( srcPaths , destPaths ) : void => copyFileSync ( path . join ( ... srcPaths ) , path . join ( ... destPaths ) ) ; param paths export const checkAFileExists = ( ... paths ) : boolean => existsSync ( path . join ( ... paths ) ) ; /**
 * Gets package.json path
 */ export const getPackageJsonPath = ( ) : string => path . join ( getCwd ( ) , packageJsonStr ) ; /**
 * Reads app package.json from the rootDir.
 */ export const readAppPackageJson = ( ) : AppPackageJson => { return readPackageJson ( getPackageJsonPath ( ) ) as AppPackageJson ; } ; param version export const updateAppVersion = ( version ) : void => { const packageJson = readAppPackageJson ( ) ; packageJson . version = version ; fs . writeFileSync ( getPackageJsonPath ( ) , JSON . stringify ( packageJson , null , 2 ) ) ; } ; param dir param file export const findUp = ( dir , file ) : string => { const resolved = path . resolve ( dir ) ; if ( resolved === rootDir ) { throw new Error ( `Reached OS root directory without finding ${ file }` ) ; } const filePath = path . join ( resolved , file ) ; if ( fs . existsSync ( filePath ) ) { return filePath ; } return findUp ( path . resolve ( resolved , '..' ) , file ) ; } ; /**
 * mkdir -p wrapper
 */ export const mkdirpSync = mkdirp . sync ; param source param target param variables export const copyTemplateDir = async ( source , target , variables ) : Promise < unknown > => { return promiseCopyTempDir ( source , target , variables ) ; } ; /**
 * rm -rf sync script
 */ export const rmRfSync = rimRaf . sync ; param paths export const resolveCwd = ( ... paths ) : string => resolveRelative ( getCwd ( ) , ... paths ) ; param dir param patterns export const findGlobsIn = ( dir , ... patterns ) : string [ ] => { return globbySync ( patterns , { cwd : dir } ) ; } ; param patterns export const findGlobs = ( ... patterns ) : string [ ] => { return findGlobsIn ( path . join ( getCwd ( ) , 'src' ) , ... patterns ) ; } ; param name param dir param promptForOverwrite return export const checkPluginConfigurationExists = async ( name , dir , promptForOverwrite = false ) : Promise < boolean > => { const cliPaths = getCliPaths ( ) ; if ( ! checkFilesExist ( cliPaths . pluginsJsonPath ) ) { mkdirpSync ( cliPaths . flexDir ) ; writeJSONFile ( { plugins : [ ] } , cliPaths . pluginsJsonPath ) ; } const config = readPluginsJson ( ) ; const plugin = config . plugins . find ( ( p ) => p . name === name ) ; if ( ! plugin ) { config . plugins . push ( { name , dir } ) ; writeJSONFile ( config , cliPaths . pluginsJsonPath ) ; return true ; } if ( plugin . dir === dir ) { return false ; } const answer = promptForOverwrite ? await confirm ( `You already have a plugin called ${ plugin . name } located at ${ plugin . dir }. Do you want to update it to ${ dir }?` , 'N' ) : true ; if ( answer ) { plugin . dir = dir ; writeJSONFile ( config , cliPaths . pluginsJsonPath ) ; return true ; } return false ; } ; /**
 * Touch ~/.twilio-cli/flex/locallyRunningPlugins.json if it does not exist,
 * and if it does exist, clear the file so it is ready for a new run
 */ export const checkRunPluginConfigurationExists = async ( localPlugins ) : Promise < void > => { const cliPaths = getCliPaths ( ) ; if ( ! checkFilesExist ( cliPaths . localPluginsJsonPath ) ) { mkdirpSync ( cliPaths . flexDir ) ; } const runConfig = { plugins : [ ] , loadedPlugins : [ ] } ; for ( const plugin of localPlugins ) { runConfig . plugins . push ( plugin ) ; } writeJSONFile ( runConfig , cliPaths . localPluginsJsonPath ) ; } ; /**
 * Adds the node_modules to the app module.
 * This is needed because we spawn different scripts when running start/build/test and so we lose
 * the original cwd directory
 */ export const addCWDNodeModule = ( ... args ) : void => { const indexCoreCwd = args . indexOf ( '--core-cwd' ) ; if ( indexCoreCwd !== - 1 ) { const coreCwd = args [ indexCoreCwd + 1 ] ; if ( coreCwd ) { setCoreCwd ( coreCwd ) ; } } const indexCwd = args . indexOf ( '--cwd' ) ; if ( indexCwd === - 1 ) { setCwd ( getCwd ( ) ) ; } else { const cwd = args [ indexCwd + 1 ] ; if ( cwd ) { setCwd ( cwd ) ; } } } ; param pkg export const resolveModulePath = ( pkg , ... paths ) : string | false => { try { return require . resolve ( pkg ) ; } catch { const requirePaths = ( require . main && require . main . paths ) || [ ] ; requirePaths . push ( ... paths ) ; try { return require . resolve ( pkg , { paths : requirePaths } ) ; } catch { return false ; } } } ; /**
 * Returns the path to flex-plugin-scripts
 */ export const _getFlexPluginScripts = ( ) : string => { const flexPluginScriptPath = resolveModulePath ( 'flex-plugin-scripts' ) ; if ( flexPluginScriptPath === false ) { throw new Error ( 'Could not resolve flex-plugin-scripts' ) ; } return path . join ( path . dirname ( flexPluginScriptPath ) , '..' ) ; } ; /**
 * Returns the path to flex-plugin-webpack
 */ export const _getFlexPluginWebpackPath = ( scriptsNodeModulesDir ) : string => { const flexPluginWebpackPath = resolveModulePath ( 'flex-plugin-webpack' , scriptsNodeModulesDir ) ; if ( flexPluginWebpackPath === false ) { throw new Error ( `Could not resolve flex-plugin-webpack` ) ; } return path . join ( path . dirname ( flexPluginWebpackPath ) , '..' ) ; } ; /**
 * Returns the paths to all modules and directories used in the plugin-builder
 */ export const getPaths = ( ) => { const cwd = getCwd ( ) ; const nodeModulesDir = resolveCwd ( 'node_modules' ) ; const scriptsDir = _getFlexPluginScripts ( ) ; const scriptsNodeModulesDir = resolveRelative ( scriptsDir , 'node_modules' ) ; const devAssetsDir = resolveRelative ( scriptsDir , 'dev_assets' ) ; const publicDir = resolveCwd ( 'public' ) ; const buildDir = resolveCwd ( 'build' ) ; const srcDir = resolveCwd ( 'src' ) ; const flexUIDir = resolveRelative ( nodeModulesDir , flexUI ) ; const tsConfigPath = resolveCwd ( 'tsconfig.json' ) ; const webpackDir = _getFlexPluginWebpackPath ( scriptsNodeModulesDir ) ; let pkgName = '' ; let pkgVersion = '' ; try { const pkg = readAppPackageJson ( ) ; pkgName = pkg . name ; pkgVersion = pkg . version ; } catch ( e ) { } return { cwd , webpack : { dir : webpackDir , nodeModulesDir : resolveRelative ( webpackDir , 'node_modules' ) , } , scripts : { dir : scriptsDir , nodeModulesDir : scriptsNodeModulesDir , devAssetsDir , indexHTMLPath : resolveRelative ( devAssetsDir , 'index.html' ) , tsConfigPath : resolveRelative ( devAssetsDir , 'tsconfig.json' ) , } , cli : getCliPaths ( ) , app : { dir : cwd , name : pkgName , version : pkgVersion , pkgPath : resolveCwd ( packageJsonStr ) , jestConfigPath : resolveCwd ( 'jest.config.js' ) , webpackConfigPath : resolveCwd ( 'webpack.config.js' ) , devServerConfigPath : resolveCwd ( 'webpack.dev.js' ) , tsConfigPath , isTSProject : ( ) => checkFilesExist ( tsConfigPath ) , setupTestsPaths : [ resolveCwd ( 'setupTests.js' ) , resolveRelative ( srcDir , 'setupTests.js' ) ] , envPath : resolveCwd ( '/.env' ) , hasEnvFile : ( ) => checkFilesExist ( resolveCwd ( '/.env' ) ) , envExamplePath : resolveCwd ( '/.env.example' ) , hasEnvExampleFile : ( ) => checkFilesExist ( resolveCwd ( '/.env.example' ) ) , envDefaultsPath : resolveCwd ( '/.env.defaults' ) , hasEnvDefaultsPath : ( ) => checkFilesExist ( resolveCwd ( '/.env.defaults' ) ) , buildDir , bundlePath : resolveRelative ( buildDir , pkgName , '.js' ) , sourceMapPath : resolveRelative ( buildDir , pkgName , '.js.map' ) , srcDir , entryPath : resolveRelative ( srcDir , 'index' ) , nodeModulesDir , flexUIDir , flexUIPkgPath : resolveRelative ( flexUIDir , packageJsonStr ) , publicDir , appConfig : resolveRelative ( publicDir , 'appConfig.js' ) , dependencies : { react : { version : readPackageJson ( resolveRelative ( nodeModulesDir , react , packageJsonStr ) ) . version , } , reactDom : { version : readPackageJson ( resolveRelative ( nodeModulesDir , reactDOM , packageJsonStr ) ) . version , } , flexUI : { version : readPackageJson ( resolveRelative ( nodeModulesDir , flexUI , packageJsonStr ) ) . version , } , } , } , assetBaseUrlTemplate : `/plugins/${ pkgName }/%PLUGIN_VERSION%` , extensions : [ 'js' , 'mjs' , 'jsx' , 'ts' , 'tsx' ] , } ; } ; param pkgName return export const getDependencyVersion = ( pkgName ) : string => { try { return _require ( `${ pkgName }/package.json` ) . version ; } catch { try { return _require ( resolveRelative ( getPaths ( ) . app . nodeModulesDir , pkgName , packageJsonStr ) ) . version ; } catch { return _require ( resolveRelative ( getPaths ( ) . scripts . nodeModulesDir , pkgName , packageJsonStr ) ) . version ; } } } ; param name export const getPackageVersion = ( name ) : string => { const installedPath = resolveRelative ( getPaths ( ) . app . nodeModulesDir , name , packageJsonStr ) ; return readPackageJson ( installedPath ) . version ; } ; param packageJson export const isPluginDir = ( packageJson ) : boolean => { return Boolean ( packageJson . dependencies [ flexUI ] || packageJson . devDependencies [ flexUI ] ) ; } ; param pkg param name export const packageDependencyVersion = ( pkg , name ) : string | null => { if ( pkg . dependencies && name in pkg . dependencies ) { return pkg . dependencies [ name ] ; } if ( pkg . devDependencies && name in pkg . devDependencies ) { return pkg . devDependencies [ name ] ; } if ( pkg . peerDependencies && name in pkg . peerDependencies ) { return pkg . peerDependencies [ name ] ; } return null ; } ; param name export const flexUIPackageDependencyVersion = ( name ) : string | null => { return packageDependencyVersion ( _require ( getPaths ( ) . app . flexUIPkgPath ) as PackageJson , name ) ; } ;