module twilio-labs import debug from 'debug' ; import fs from 'fs' ; import path , { extname } from 'path' ; import { toUnix } from 'upath' ; import recursiveReadDir from 'recursive-readdir' ; import { promisify } from 'util' ; import { AccessOptions , DirectoryContent , FileInfo , ResourcePathAndAccess , ServerlessResourceConfigWithFilePath , } from '../types' ; const log = debug ( 'twilio-serverless-api:fs' ) ; export const access = promisify ( fs . access ) ; export const readFile = promisify ( fs . readFile ) ; export const writeFile = promisify ( fs . writeFile ) ; export const readDir = promisify ( recursiveReadDir ) ; export const stat = promisify ( fs . stat ) ; export param { string } filePath returns export async function fileExists ( filePath ) { try { await access ( filePath , fs . constants . R_OK | fs . constants . W_OK ) ; return true ; } catch ( err ) { return false ; } } export type ValidPathResult = { valid : true ; } | { valid : false ; message : string ; } ; param path export function checkForValidPath ( path ) { if ( ! path . startsWith ( '/' ) ) { return { valid : false , message : `Expected path to start with "/". Got: "${ path }"` , } ; } if ( path . includes ( '#' ) ) { return { valid : false , message : `Path cannot contain a #. Got: "${ path }"` , } ; } const invalidCharacters = /[;,\?:\@\+&\$\(\)' "]/g ; if ( invalidCharacters . test ( path ) ) { return { valid : false , message : `Path cannot contain any of the following characters: ;,?:@+&$()' ". Got: "${ path }"` , } ; } if ( path . length >= 256 ) { return { valid : false , message : `Path length must be shorter than 256 characters. Got: ${ path . length } characters.` , } ; } return { valid : true } ; } export param { FileInfo } file param { string } ignoreExtension returns { ResourcePathAndAccess } export function getPathAndAccessFromFileInfo ( file , ignoreExtension ? ) { const relativePath = path . dirname ( file . name ) ; let access = 'public' ; const ext = path . extname ( file . name ) ; let baseName = path . basename ( file . name , ext ) ; if ( file . name . includes ( `.protected${ ext }` ) ) { access = 'protected' ; } else if ( file . name . includes ( `.private${ ext }` ) ) { access = 'private' ; } baseName = baseName . replace ( `.${ access }` , '' ) ; let resourcePath = `/` + path . join ( relativePath , baseName ) ; if ( ext !== ignoreExtension ) { resourcePath += ext ; } resourcePath = resourcePath . replace ( /\s/g , '-' ) ; resourcePath = toUnix ( resourcePath ) ; const validatedPath = checkForValidPath ( resourcePath ) ; if ( ! validatedPath . valid ) { throw new Error ( validatedPath . message ) ; } return { path : resourcePath , access , } ; } export param { string } dir param { string } extension returns { Promise < FileInfo [ ] > } export async function getDirContent ( dir , extension ? ) { const rawFiles = ( await readDir ( dir ) ) as string [ ] ; const unfilteredFiles = await Promise . all ( rawFiles . map ( async ( filePath ) => { if ( ! path . isAbsolute ( filePath ) ) { filePath = path . join ( dir , filePath ) ; } const entry = await stat ( filePath ) ; if ( ! entry . isFile ( ) ) { return undefined ; } if ( extension && path . extname ( filePath ) !== extension ) { return undefined ; } if ( path . basename ( filePath ) === '.DS_Store' ) { return undefined ; } const name = path . relative ( dir , filePath ) ; return { name : name , path : filePath , } ; } ) ) ; return unfilteredFiles . filter ( entry => typeof entry !== 'undefined' ) as FileInfo [ ] ; } export param { string } basePath param { string [ ] } directories returns { string } export function getFirstMatchingDirectory ( basePath , directories ) { for ( let dir of directories ) { const fullPath = path . join ( basePath , dir ) ; try { const fStat = fs . statSync ( fullPath ) ; if ( fStat . isDirectory ( ) ) { return fullPath ; } } catch ( err ) { continue ; } } throw new Error ( `Could not find any of these directories "${ directories . join ( '", "' ) }"` ) ; } export type SearchConfig = { type { string [ ] } functionsFolderNames ? : string [ ] ; type { string [ ] } assetsFolderNames ? : string [ ] ; } ; export param { string } cwd param { SearchConfig } config returns { Promise < DirectoryContent > } export async function getListOfFunctionsAndAssets ( cwd , config = { } ) { let functionsDir ; try { const possibleFunctionDirs = config . functionsFolderNames || [ 'functions' , 'src' , ] ; log ( 'Search for directory. Options: "%s"' , possibleFunctionDirs . join ( ',' ) ) ; functionsDir = getFirstMatchingDirectory ( cwd , possibleFunctionDirs ) ; } catch ( err ) { functionsDir = undefined ; } log ( 'Found Functions Directory "%s"' , functionsDir ) ; let assetsDir ; try { const possibleAssetDirs = config . assetsFolderNames || [ 'assets' , 'static' ] ; log ( 'Search for directory. Options: "%s"' , possibleAssetDirs . join ( ',' ) ) ; assetsDir = getFirstMatchingDirectory ( cwd , possibleAssetDirs ) ; } catch ( err ) { assetsDir = undefined ; } log ( 'Found Assets Directory "%s"' , assetsDir ) ; const functionFiles = functionsDir ? await getDirContent ( functionsDir , '.js' ) : [ ] ; const functionConfigs = await getServerlessConfigs ( functionFiles , '.js' ) ; const assetFiles = assetsDir ? await getDirContent ( assetsDir ) : [ ] ; const assetConfigs = await getServerlessConfigs ( assetFiles ) ; return { functions : functionConfigs , assets : assetConfigs } ; } param { FileInfo [ ] } dirContent param { string } ignoreExtension returns { Promise < ServerlessResourceConfigWithFilePath [ ] > } async function getServerlessConfigs ( dirContent , ignoreExtension ? ) { return Promise . all ( dirContent . map ( async ( file ) => { const { path , access } = getPathAndAccessFromFileInfo ( file , ignoreExtension ) ; const encoding = extname ( file . path ) === '.js' ? 'utf8' : undefined ; const content = await readFile ( file . path , encoding ) ; return { name : path , path , access , content , filePath : file . path , } ; } ) ) ; }